Searchbar, state mngmt. etc.. #3

Merged
christian merged 6 commits from searchbar into main 2024-05-25 23:51:26 +00:00
10 changed files with 262 additions and 43 deletions

54
package-lock.json generated
View File

@ -15,11 +15,14 @@
"@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",
@ -27,13 +30,14 @@
"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": "^18.2.57", "@types/react": "npm:types-react@rc",
"@types/react-dom": "^18.2.19", "@types/react-dom": "npm:types-react-dom@rc",
"@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",
@ -5226,6 +5230,15 @@
"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",
@ -8027,6 +8040,14 @@
} }
} }
}, },
"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",
@ -8313,6 +8334,33 @@
"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
}
}
} }
} }
} }

View File

@ -19,11 +19,14 @@
"@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",
@ -31,13 +34,18 @@
"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": "^18.2.57", "@types/react": "npm:types-react@rc",
"@types/react-dom": "^18.2.19", "@types/react-dom": "npm:types-react-dom@rc",
"@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",

View File

@ -1,9 +1,38 @@
"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>
); );
} }

View File

@ -10,28 +10,33 @@ 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 (

View File

@ -0,0 +1,5 @@
import { Input } from "~/components/ui/input";
export default function SearchBar() {
return <Input className="mx-auto max-w-xl focus-visible:ring-0" />;
}

View File

@ -1,3 +0,0 @@
export default function WineName(props: { name: string }) {
return <p>{props.name}</p>;
}

View File

@ -1,25 +0,0 @@
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);
}

View File

@ -4,6 +4,7 @@ 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"],
@ -29,7 +30,10 @@ export default function RootLayout({
fontSans.variable, fontSans.variable,
)} )}
> >
<Filtermenu /> <div className="container pt-12">
<SearchBar />
<Filtermenu />
</div>
{children} {children}
</body> </body>
</html> </html>

156
src/app/store.ts Normal file
View File

@ -0,0 +1,156 @@
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;

View File

@ -1,8 +0,0 @@
export interface Producer {
id: string;
name: string;
createdAt: Date;
country: string;
region: string;
email: string;
}