Compare commits
No commits in common. "0a4f5017ea352e9509f15399feb1c0b5368ca569" and "fcfc84871959ff9ea6aa32abcd7f942e36a1ba64" have entirely different histories.
0a4f5017ea
...
fcfc848719
54
package-lock.json
generated
54
package-lock.json
generated
@ -15,14 +15,11 @@
|
|||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@t3-oss/env-nextjs": "^0.10.1",
|
"@t3-oss/env-nextjs": "^0.10.1",
|
||||||
"@types/react": "npm:types-react@rc",
|
|
||||||
"@types/react-dom": "npm:types-react-dom@rc",
|
|
||||||
"@vercel/postgres": "^0.8.0",
|
"@vercel/postgres": "^0.8.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"drizzle-orm": "^0.29.4",
|
"drizzle-orm": "^0.29.4",
|
||||||
"geist": "^1.3.0",
|
"geist": "^1.3.0",
|
||||||
"immer": "^10.1.1",
|
|
||||||
"lucide-react": "^0.379.0",
|
"lucide-react": "^0.379.0",
|
||||||
"next": "^15.0.0-rc.0",
|
"next": "^15.0.0-rc.0",
|
||||||
"postgres": "^3.4.3",
|
"postgres": "^3.4.3",
|
||||||
@ -30,14 +27,13 @@
|
|||||||
"react-dom": "^19.0.0-rc-935180c7e0-20240524",
|
"react-dom": "^19.0.0-rc-935180c7e0-20240524",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8"
|
||||||
"zustand": "^4.5.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/eslint": "^8.56.2",
|
"@types/eslint": "^8.56.2",
|
||||||
"@types/node": "^20.11.20",
|
"@types/node": "^20.11.20",
|
||||||
"@types/react": "npm:types-react@rc",
|
"@types/react": "^18.2.57",
|
||||||
"@types/react-dom": "npm:types-react-dom@rc",
|
"@types/react-dom": "^18.2.19",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
||||||
"@typescript-eslint/parser": "^7.1.1",
|
"@typescript-eslint/parser": "^7.1.1",
|
||||||
"drizzle-kit": "^0.21.0",
|
"drizzle-kit": "^0.21.0",
|
||||||
@ -5230,15 +5226,6 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/immer": {
|
|
||||||
"version": "10.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
|
||||||
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/immer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||||
@ -8040,14 +8027,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/use-sync-external-store": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/utf-8-validate": {
|
"node_modules/utf-8-validate": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.3.tgz",
|
||||||
@ -8334,33 +8313,6 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/zustand": {
|
|
||||||
"version": "4.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz",
|
|
||||||
"integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==",
|
|
||||||
"dependencies": {
|
|
||||||
"use-sync-external-store": "1.2.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.7.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": ">=16.8",
|
|
||||||
"immer": ">=9.0.6",
|
|
||||||
"react": ">=16.8"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"immer": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
package.json
14
package.json
@ -19,14 +19,11 @@
|
|||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@t3-oss/env-nextjs": "^0.10.1",
|
"@t3-oss/env-nextjs": "^0.10.1",
|
||||||
"@types/react": "npm:types-react@rc",
|
|
||||||
"@types/react-dom": "npm:types-react-dom@rc",
|
|
||||||
"@vercel/postgres": "^0.8.0",
|
"@vercel/postgres": "^0.8.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"drizzle-orm": "^0.29.4",
|
"drizzle-orm": "^0.29.4",
|
||||||
"geist": "^1.3.0",
|
"geist": "^1.3.0",
|
||||||
"immer": "^10.1.1",
|
|
||||||
"lucide-react": "^0.379.0",
|
"lucide-react": "^0.379.0",
|
||||||
"next": "^15.0.0-rc.0",
|
"next": "^15.0.0-rc.0",
|
||||||
"postgres": "^3.4.3",
|
"postgres": "^3.4.3",
|
||||||
@ -34,18 +31,13 @@
|
|||||||
"react-dom": "^19.0.0-rc-935180c7e0-20240524",
|
"react-dom": "^19.0.0-rc-935180c7e0-20240524",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8"
|
||||||
"zustand": "^4.5.2"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"@types/react": "npm:types-react@rc",
|
|
||||||
"@types/react-dom": "npm:types-react-dom@rc"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/eslint": "^8.56.2",
|
"@types/eslint": "^8.56.2",
|
||||||
"@types/node": "^20.11.20",
|
"@types/node": "^20.11.20",
|
||||||
"@types/react": "npm:types-react@rc",
|
"@types/react": "^18.2.57",
|
||||||
"@types/react-dom": "npm:types-react-dom@rc",
|
"@types/react-dom": "^18.2.19",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
||||||
"@typescript-eslint/parser": "^7.1.1",
|
"@typescript-eslint/parser": "^7.1.1",
|
||||||
"drizzle-kit": "^0.21.0",
|
"drizzle-kit": "^0.21.0",
|
||||||
|
@ -1,38 +1,9 @@
|
|||||||
"use client";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import FormCard from "./_components/FormCard";
|
import FormCard from "./_components/FormCard";
|
||||||
import useFilterStore from "./store";
|
|
||||||
|
|
||||||
const AddRegionButton: React.FC = () => {
|
|
||||||
const addRegion = useFilterStore((state) => state.addRegion);
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
addRegion("sverige");
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Button onClick={handleClick}>Add Region "Sverige"</Button>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ResetFilters: React.FC = () => {
|
|
||||||
const resetFilters = useFilterStore((state) => state.resetFilters);
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
resetFilters();
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Button onClick={handleClick}>Reset filters</Button>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const filters = useFilterStore((state) => state.filters);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container flex w-full flex-col justify-center">
|
<div className="container flex w-full flex-col justify-center">
|
||||||
<FormCard />
|
<FormCard />
|
||||||
<h1 className="text-2xl">Filter state:</h1>
|
|
||||||
{JSON.stringify(filters)}
|
|
||||||
<AddRegionButton />
|
|
||||||
<ResetFilters />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,33 +10,28 @@ import {
|
|||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Slider } from "~/components/ui/slider";
|
import { Slider } from "~/components/ui/slider";
|
||||||
import { ChangeEvent, useState } from "react";
|
import { ChangeEvent, useState } from "react";
|
||||||
import useFilterStore from "../store";
|
|
||||||
|
|
||||||
export default function Filtermenu() {
|
export default function Filtermenu() {
|
||||||
const [minPrice, setMinPrice] = useState<number>(0);
|
const [minPrice, setMinPrice] = useState<number>(0);
|
||||||
const [maxPrice, setMaxPrice] = useState<number>(9999);
|
const [maxPrice, setMaxPrice] = useState<number>(9999);
|
||||||
const [sliderValues, setSliderValues] = useState<[number, number]>([0, 9999]);
|
const [sliderValues, setSliderValues] = useState<[number, number]>([0, 9999]);
|
||||||
const setStorePrice = useFilterStore((state) => state.setPrice);
|
|
||||||
|
|
||||||
const handleMinPriceChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
const handleMinPriceChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
const value = Math.min(Number(e.target.value), maxPrice - 1);
|
const value = Math.min(Number(e.target.value), maxPrice - 1);
|
||||||
setMinPrice(value);
|
setMinPrice(value);
|
||||||
setSliderValues([value, sliderValues[1]]);
|
setSliderValues([value, sliderValues[1]]);
|
||||||
setStorePrice(minPrice, maxPrice);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMaxPriceChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleMaxPriceChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = Math.max(Number(e.target.value), minPrice + 1);
|
const value = Math.max(Number(e.target.value), minPrice + 1);
|
||||||
setMaxPrice(value);
|
setMaxPrice(value);
|
||||||
setSliderValues([sliderValues[0], value]);
|
setSliderValues([sliderValues[0], value]);
|
||||||
setStorePrice(minPrice, maxPrice);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSliderChange = (values: [number, number]) => {
|
const handleSliderChange = (values: [number, number]) => {
|
||||||
setSliderValues(values);
|
setSliderValues(values);
|
||||||
setMinPrice(values[0]);
|
setMinPrice(values[0]);
|
||||||
setMaxPrice(values[1]);
|
setMaxPrice(values[1]);
|
||||||
setStorePrice(minPrice, maxPrice);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import { Input } from "~/components/ui/input";
|
|
||||||
|
|
||||||
export default function SearchBar() {
|
|
||||||
return <Input className="mx-auto max-w-xl focus-visible:ring-0" />;
|
|
||||||
}
|
|
3
src/app/_components/WineName.tsx
Normal file
3
src/app/_components/WineName.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function WineName(props: { name: string }) {
|
||||||
|
return <p>{props.name}</p>;
|
||||||
|
}
|
25
src/app/_components/WineProducer.tsx
Normal file
25
src/app/_components/WineProducer.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import getProducer from "~/server/actions/getProducer";
|
||||||
|
|
||||||
|
export default async function WineProducer(props: { id: string }) {
|
||||||
|
const { id } = props;
|
||||||
|
|
||||||
|
// Validate the id before making the database call
|
||||||
|
if (!id) {
|
||||||
|
return <p>Invalid producer ID</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getProducer(id);
|
||||||
|
return !result ? <p>Unable to retrieve producer</p> : <p>{result?.name}</p>;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching producer:", error);
|
||||||
|
return <p>Error fetching producer</p>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility function to validate UUID
|
||||||
|
function isValidUUID(uuid: string) {
|
||||||
|
const uuidRegex =
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
return uuidRegex.test(uuid);
|
||||||
|
}
|
@ -4,7 +4,6 @@ import { Inter as FontSans } from "next/font/google";
|
|||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import TopNav from "./_components/FilterMenu";
|
import TopNav from "./_components/FilterMenu";
|
||||||
import Filtermenu from "./_components/FilterMenu";
|
import Filtermenu from "./_components/FilterMenu";
|
||||||
import SearchBar from "./_components/SearchBar";
|
|
||||||
|
|
||||||
const fontSans = FontSans({
|
const fontSans = FontSans({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@ -30,10 +29,7 @@ export default function RootLayout({
|
|||||||
fontSans.variable,
|
fontSans.variable,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="container pt-12">
|
|
||||||
<SearchBar />
|
|
||||||
<Filtermenu />
|
<Filtermenu />
|
||||||
</div>
|
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
156
src/app/store.ts
156
src/app/store.ts
@ -1,156 +0,0 @@
|
|||||||
import { create } from "zustand";
|
|
||||||
import { produce } from "immer";
|
|
||||||
import { persist, createJSONStorage } from "zustand/middleware";
|
|
||||||
|
|
||||||
interface FilterState {
|
|
||||||
filters: Filters;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Filters {
|
|
||||||
searchQuery: string;
|
|
||||||
regions: string[];
|
|
||||||
countries: string[];
|
|
||||||
producers: string[];
|
|
||||||
price: Price;
|
|
||||||
type: WineType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Price {
|
|
||||||
min: number;
|
|
||||||
max: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WineType {
|
|
||||||
sparkling: boolean;
|
|
||||||
white: boolean;
|
|
||||||
red: boolean;
|
|
||||||
sweet: boolean;
|
|
||||||
other: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilterActions = {
|
|
||||||
addRegion: (region: string) => void;
|
|
||||||
removeRegion: (region: string) => void;
|
|
||||||
addCountry: (country: string) => void;
|
|
||||||
removeCountry: (country: string) => void;
|
|
||||||
addProducer: (producer: string) => void;
|
|
||||||
removeProducer: (producer: string) => void;
|
|
||||||
setPrice: (min: number, max: number) => void;
|
|
||||||
setType: (type: keyof WineType, value: boolean) => void;
|
|
||||||
resetFilters: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialFilters: Filters = {
|
|
||||||
searchQuery: "",
|
|
||||||
regions: [],
|
|
||||||
countries: [],
|
|
||||||
producers: [],
|
|
||||||
price: {
|
|
||||||
min: 0,
|
|
||||||
max: 9999,
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
sparkling: false,
|
|
||||||
white: false,
|
|
||||||
red: false,
|
|
||||||
sweet: false,
|
|
||||||
other: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const useFilterStore = create<FilterState & FilterActions>()(
|
|
||||||
persist(
|
|
||||||
(set) => ({
|
|
||||||
filters: {
|
|
||||||
searchQuery: "",
|
|
||||||
regions: [],
|
|
||||||
countries: [],
|
|
||||||
producers: [],
|
|
||||||
price: {
|
|
||||||
min: 0,
|
|
||||||
max: 9999,
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
sparkling: false,
|
|
||||||
white: false,
|
|
||||||
red: false,
|
|
||||||
sweet: false,
|
|
||||||
other: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
addRegion: (region) =>
|
|
||||||
set(
|
|
||||||
produce((state: FilterState) => {
|
|
||||||
if (!state.filters.regions.includes(region)) {
|
|
||||||
state.filters.regions.push(region);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
removeRegion: (region) =>
|
|
||||||
set(
|
|
||||||
produce((state: FilterState) => {
|
|
||||||
state.filters.regions = state.filters.regions.filter(
|
|
||||||
(r) => r !== region,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
addCountry: (country) =>
|
|
||||||
set(
|
|
||||||
produce((state: FilterState) => {
|
|
||||||
if (!state.filters.countries.includes(country)) {
|
|
||||||
state.filters.countries.push(country);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
removeCountry: (country) =>
|
|
||||||
set(
|
|
||||||
produce((state: FilterState) => {
|
|
||||||
state.filters.countries = state.filters.countries.filter(
|
|
||||||
(c) => c !== country,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
addProducer: (producer) =>
|
|
||||||
set(
|
|
||||||
produce((state: FilterState) => {
|
|
||||||
if (!state.filters.producers.includes(producer)) {
|
|
||||||
state.filters.producers.push(producer);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
removeProducer: (producer) =>
|
|
||||||
set(
|
|
||||||
produce((state: FilterState) => {
|
|
||||||
state.filters.producers = state.filters.producers.filter(
|
|
||||||
(p) => p !== producer,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
setPrice: (min, max) =>
|
|
||||||
set(
|
|
||||||
produce((state: FilterState) => {
|
|
||||||
state.filters.price.min = min;
|
|
||||||
state.filters.price.max = max;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
setType: (type, value) =>
|
|
||||||
set(
|
|
||||||
produce((state: FilterState) => {
|
|
||||||
state.filters.type[type] = value;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
resetFilters: () =>
|
|
||||||
set(
|
|
||||||
produce((state: FilterState) => {
|
|
||||||
state.filters = initialFilters;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: "filter-storage", // name of the item in the storage (must be unique)
|
|
||||||
storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
export default useFilterStore;
|
|
8
src/types/types.ts
Normal file
8
src/types/types.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface Producer {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
createdAt: Date;
|
||||||
|
country: string;
|
||||||
|
region: string;
|
||||||
|
email: string;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user