Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
6137a166eb |
22
README.md
22
README.md
|
@ -1,34 +1,18 @@
|
||||||
This is my attempt at a ecommerce site selling wine.
|
This is my attempt at a ecommerce site selling wine.
|
||||||
|
|
||||||
Stack:
|
Created with t3-app using:
|
||||||
|
|
||||||
- nextjs
|
- nextjs
|
||||||
- tailwind
|
- tailwind
|
||||||
- drizzle
|
- drizzle
|
||||||
- postgres
|
- postgres
|
||||||
- zod
|
|
||||||
- zustand
|
|
||||||
- clerk
|
|
||||||
|
|
||||||
TODO (in no particular order for now):
|
TODO (in no particular order for now):
|
||||||
|
|
||||||
- [] State management
|
- [] Top nav
|
||||||
- [] Filter Menu
|
|
||||||
|
|
||||||
- [] Populate filter menues
|
|
||||||
- [] Search bar
|
|
||||||
- [] Filter logic
|
|
||||||
|
|
||||||
- [x] define (and draw?) db schema (initial)
|
- [x] define (and draw?) db schema (initial)
|
||||||
- [] define final schema
|
- [] side product nav
|
||||||
|
|
||||||
- [] region
|
|
||||||
- [] country
|
|
||||||
- [] price
|
|
||||||
|
|
||||||
- [] company burger/accordian
|
|
||||||
- [] product cards
|
- [] product cards
|
||||||
- [] cart
|
- [] cart
|
||||||
- [] auth & login
|
- [] auth & login
|
||||||
- [] user profile page
|
- [] user profile page
|
||||||
- [] admin dashboad
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
|
||||||
"style": "default",
|
|
||||||
"rsc": true,
|
|
||||||
"tsx": true,
|
|
||||||
"tailwind": {
|
|
||||||
"config": "tailwind.config.ts",
|
|
||||||
"css": "src/styles/globals.css",
|
|
||||||
"baseColor": "slate",
|
|
||||||
"cssVariables": false,
|
|
||||||
"prefix": ""
|
|
||||||
},
|
|
||||||
"aliases": {
|
|
||||||
"components": "~/components",
|
|
||||||
"utils": "~/lib/utils"
|
|
||||||
}
|
|
||||||
}
|
|
4245
package-lock.json
generated
4245
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
|
@ -12,25 +12,17 @@
|
||||||
"start": "next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-navigation-menu": "^1.1.4",
|
"@nextui-org/react": "^2.3.6",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
|
||||||
"@radix-ui/react-slider": "^1.1.2",
|
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
|
||||||
"@t3-oss/env-nextjs": "^0.10.1",
|
"@t3-oss/env-nextjs": "^0.10.1",
|
||||||
"@types/react": "^18.2.57",
|
|
||||||
"@types/react-dom": "^18.2.19",
|
|
||||||
"@vercel/postgres": "^0.8.0",
|
"@vercel/postgres": "^0.8.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"drizzle-orm": "^0.29.4",
|
"drizzle-orm": "^0.29.4",
|
||||||
"immer": "^10.1.1",
|
"framer-motion": "^11.2.6",
|
||||||
"lucide-react": "^0.379.0",
|
"geist": "^1.3.0",
|
||||||
"next": "^14.2.3",
|
"next": "^15.0.0-rc.0",
|
||||||
"postgres": "^3.4.3",
|
"postgres": "^3.4.3",
|
||||||
"tailwind-merge": "^2.3.0",
|
"react": "^19.0.0-rc-935180c7e0-20240524",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"react-dom": "^19.0.0-rc-935180c7e0-20240524",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8"
|
||||||
"zustand": "^4.5.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/eslint": "^8.56.2",
|
"@types/eslint": "^8.56.2",
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import WineList from "./_components/WineList";
|
|
||||||
import FilterStatus from "./_components/FilterState";
|
|
||||||
import CreateCountry from "./_components/admin/CreateCountry";
|
|
||||||
import AllCountries from "./_components/AllCountries";
|
|
||||||
import CreateRegion from "./_components/admin/CreateRegion";
|
|
||||||
import AllRegions from "./_components/AllRegions";
|
|
||||||
import CreateSubRegion from "./_components/admin/CreateSubRegion";
|
|
||||||
import AllSubRegions from "./_components/AllSubRegions";
|
|
||||||
import CreateProducer from "./_components/admin/CreateProducer";
|
|
||||||
import AllProducers from "./_components/allProducers";
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
|
||||||
<div className="container flex w-full flex-col justify-center">
|
|
||||||
<FilterStatus />
|
|
||||||
<WineList />
|
|
||||||
<CreateCountry />
|
|
||||||
<AllCountries />
|
|
||||||
<CreateRegion />
|
|
||||||
<AllRegions />
|
|
||||||
<CreateSubRegion />
|
|
||||||
<AllSubRegions />
|
|
||||||
<CreateProducer />
|
|
||||||
<AllProducers />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { Delete } from "lucide-react";
|
|
||||||
import { deleteCountry } from "~/server/actions/deleteCountry";
|
|
||||||
import getAllCountries from "~/server/actions/getAllCountries";
|
|
||||||
|
|
||||||
export default async function AllCountries() {
|
|
||||||
const countries = await getAllCountries();
|
|
||||||
return (
|
|
||||||
<div className="pt-4">
|
|
||||||
<h1 className="text-2xl">All Countries:</h1>
|
|
||||||
{countries.map((country) => (
|
|
||||||
<div key={country.id} className="flex gap-1">
|
|
||||||
<p>{country.name}</p>
|
|
||||||
<form action={deleteCountry.bind(null, country.id)}>
|
|
||||||
<button>
|
|
||||||
<Delete />
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { Delete } from "lucide-react";
|
|
||||||
import deleteRegion from "~/server/actions/deleteRegion";
|
|
||||||
import getAllCountries from "~/server/actions/getAllCountries";
|
|
||||||
import getAllRegions from "~/server/actions/getAllRegions";
|
|
||||||
|
|
||||||
export default async function AllRegions() {
|
|
||||||
const allRegions = await getAllRegions();
|
|
||||||
const allCountries = await getAllCountries();
|
|
||||||
return (
|
|
||||||
<div className="pt-4">
|
|
||||||
<h1 className="text-2xl">All Regions:</h1>
|
|
||||||
{allRegions.map((region) => (
|
|
||||||
<div key={region.id} className="flex gap-1">
|
|
||||||
<p>{region.name}</p>
|
|
||||||
<form action={deleteRegion.bind(null, region.id)}>
|
|
||||||
<button>
|
|
||||||
<Delete />
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { Delete } from "lucide-react";
|
|
||||||
import deleteSubRegion from "~/server/actions/deleteSubRegion";
|
|
||||||
import getAllSubRegions from "~/server/actions/getAllSubRegions";
|
|
||||||
|
|
||||||
export default async function AllSubRegions() {
|
|
||||||
const allSubRegions = await getAllSubRegions();
|
|
||||||
return (
|
|
||||||
<div className="pt-4">
|
|
||||||
<h1 className="text-2xl">All Sub Regions:</h1>
|
|
||||||
{allSubRegions.map((subRegion) => (
|
|
||||||
<div key={subRegion.id} className="flex gap-1">
|
|
||||||
<p>{subRegion.name}</p>
|
|
||||||
<form action={deleteSubRegion.bind(null, subRegion.id)}>
|
|
||||||
<button>
|
|
||||||
<Delete />
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
10
src/app/_components/App.tsx
Normal file
10
src/app/_components/App.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import FormCard from "./FormCard";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<div className="container flex w-full flex-col justify-center">
|
||||||
|
<h1>Yes hello</h1>
|
||||||
|
<FormCard />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
54
src/app/_components/CreateWine.tsx
Normal file
54
src/app/_components/CreateWine.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
"use client";
|
||||||
|
import { useActionState, useEffect, useState } from "react";
|
||||||
|
import { addWine } from "~/server/actions/addWine";
|
||||||
|
import { getProducers } from "~/server/actions/allProducers";
|
||||||
|
import { Button } from "@nextui-org/react";
|
||||||
|
|
||||||
|
async function fetchProducers() {
|
||||||
|
const producers = await getProducers();
|
||||||
|
return producers;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProducersData = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
createdAt: Date;
|
||||||
|
country: string;
|
||||||
|
region: string;
|
||||||
|
email: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export default function CreateWine() {
|
||||||
|
const [formState, formAction] = useActionState(addWine, {
|
||||||
|
message: "",
|
||||||
|
errors: undefined,
|
||||||
|
fieldValues: {
|
||||||
|
name: "",
|
||||||
|
producer: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [producers, setProducers] = useState<ProducersData>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadProducers() {
|
||||||
|
const producersData = await fetchProducers();
|
||||||
|
setProducers(producersData);
|
||||||
|
}
|
||||||
|
loadProducers();
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div className="container flex flex-col gap-4">
|
||||||
|
<form action={formAction}>
|
||||||
|
<input className="border-2" type="text" name="name" />
|
||||||
|
<select name="producer">
|
||||||
|
{producers.map((producer, i) => (
|
||||||
|
<option key={i} value={producer.id}>
|
||||||
|
{producer.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<Button color="primary">Add wine</Button>
|
||||||
|
</form>
|
||||||
|
{JSON.stringify(formState)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,135 +0,0 @@
|
||||||
"use client";
|
|
||||||
import {
|
|
||||||
NavigationMenu,
|
|
||||||
NavigationMenuContent,
|
|
||||||
NavigationMenuItem,
|
|
||||||
NavigationMenuList,
|
|
||||||
NavigationMenuTrigger,
|
|
||||||
} from "~/components/ui/navigation-menu";
|
|
||||||
|
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import { SliderTwoThumbs } from "~/components/ui/slider-two-thumbs";
|
|
||||||
import { ChangeEvent, useState } from "react";
|
|
||||||
import useFilterStore from "../store";
|
|
||||||
|
|
||||||
export default function Filtermenu() {
|
|
||||||
const [minPrice, setMinPrice] = useState<number>(0);
|
|
||||||
const [maxPrice, setMaxPrice] = useState<number>(9999);
|
|
||||||
const [sliderValues, setSliderValues] = useState<[number, number]>([0, 9999]);
|
|
||||||
const setStorePrice = useFilterStore((state) => state.setPrice);
|
|
||||||
|
|
||||||
const handleMinPriceChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
|
||||||
const value = Math.min(Number(e.target.value), maxPrice - 1);
|
|
||||||
setMinPrice(value);
|
|
||||||
setSliderValues([value, sliderValues[1]]);
|
|
||||||
setStorePrice(minPrice, maxPrice);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMaxPriceChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = Math.max(Number(e.target.value), minPrice + 1);
|
|
||||||
setMaxPrice(value);
|
|
||||||
setSliderValues([sliderValues[0], value]);
|
|
||||||
setStorePrice(minPrice, maxPrice);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSliderChange = (values: [number, number]) => {
|
|
||||||
setSliderValues(values);
|
|
||||||
setMinPrice(values[0]);
|
|
||||||
setMaxPrice(values[1]);
|
|
||||||
setStorePrice(values[0], values[1]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container flex justify-center gap-2">
|
|
||||||
{/* TODO:
|
|
||||||
Checkboxes instead of menuitems. Check boxes are added as badges under the search bar */}
|
|
||||||
{/* Price filter */}
|
|
||||||
<NavigationMenu>
|
|
||||||
<NavigationMenuList>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NavigationMenuTrigger>Price</NavigationMenuTrigger>
|
|
||||||
<NavigationMenuContent className="min-w- flex overflow-auto p-2">
|
|
||||||
<Input
|
|
||||||
value={minPrice}
|
|
||||||
onChange={handleMinPriceChange}
|
|
||||||
className="min-w-20"
|
|
||||||
type="number"
|
|
||||||
defaultValue={0}
|
|
||||||
min={0}
|
|
||||||
max={maxPrice - 1}
|
|
||||||
/>
|
|
||||||
<SliderTwoThumbs
|
|
||||||
value={sliderValues}
|
|
||||||
onValueChange={handleSliderChange}
|
|
||||||
className="min-w-36"
|
|
||||||
min={0}
|
|
||||||
max={9999}
|
|
||||||
defaultValue={[0, 9999]}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={maxPrice}
|
|
||||||
onChange={handleMaxPriceChange}
|
|
||||||
className="min-w-20"
|
|
||||||
type="number"
|
|
||||||
defaultValue={9999}
|
|
||||||
min={minPrice + 1}
|
|
||||||
max={9999}
|
|
||||||
/>
|
|
||||||
</NavigationMenuContent>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
</NavigationMenuList>
|
|
||||||
</NavigationMenu>
|
|
||||||
<NavigationMenu>
|
|
||||||
<NavigationMenuList>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NavigationMenuTrigger>Type</NavigationMenuTrigger>
|
|
||||||
<NavigationMenuContent>
|
|
||||||
<p>Anything goes?</p>
|
|
||||||
</NavigationMenuContent>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
</NavigationMenuList>
|
|
||||||
</NavigationMenu>
|
|
||||||
{/* Producer filter */}
|
|
||||||
<NavigationMenu>
|
|
||||||
<NavigationMenuList>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NavigationMenuTrigger>Producer</NavigationMenuTrigger>
|
|
||||||
<NavigationMenuContent className="w-fit p-2">
|
|
||||||
<Input placeholder="producer filter" />
|
|
||||||
<ul>
|
|
||||||
<li>checkboxes for producer filter</li>
|
|
||||||
</ul>
|
|
||||||
</NavigationMenuContent>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
</NavigationMenuList>
|
|
||||||
</NavigationMenu>
|
|
||||||
{/* Country Filter */}
|
|
||||||
<NavigationMenu>
|
|
||||||
<NavigationMenuList>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NavigationMenuTrigger>Producer</NavigationMenuTrigger>
|
|
||||||
<NavigationMenuContent className="w-fit p-2">
|
|
||||||
<ul>
|
|
||||||
<li>ccountry filter</li>
|
|
||||||
</ul>
|
|
||||||
</NavigationMenuContent>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
</NavigationMenuList>
|
|
||||||
</NavigationMenu>
|
|
||||||
{/* Region Filter */}
|
|
||||||
<NavigationMenu>
|
|
||||||
<NavigationMenuList>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NavigationMenuTrigger>Region</NavigationMenuTrigger>
|
|
||||||
<NavigationMenuContent className="w-fit p-2">
|
|
||||||
<Input placeholder="producer filter" />
|
|
||||||
<ul>
|
|
||||||
<li>checkboxes for producer filter</li>
|
|
||||||
</ul>
|
|
||||||
</NavigationMenuContent>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
</NavigationMenuList>
|
|
||||||
</NavigationMenu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
"use client";
|
|
||||||
import useFilterStore, { FilterState } from "../store";
|
|
||||||
|
|
||||||
export default function FilterStatus() {
|
|
||||||
const filters = useFilterStore((state) => state.filters);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1 className="text-2xl">Filter state:</h1>
|
|
||||||
{JSON.stringify(filters, null, 2)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
10
src/app/_components/FormCard.tsx
Normal file
10
src/app/_components/FormCard.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import CreateWine from "./CreateWine";
|
||||||
|
|
||||||
|
export default function FormCard() {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto p-4 shadow-md">
|
||||||
|
<h1>Add a new wine</h1>
|
||||||
|
<CreateWine />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,22 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { ChangeEvent } from "react";
|
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import useFilterStore from "../store";
|
|
||||||
|
|
||||||
export default function SearchBar() {
|
|
||||||
const setStoreSearchQuery = useFilterStore((state) => state.setSearchQuery);
|
|
||||||
const { searchQuery } = useFilterStore((state) => state.filters);
|
|
||||||
|
|
||||||
function handleInput(e: ChangeEvent<HTMLInputElement>) {
|
|
||||||
e.preventDefault();
|
|
||||||
const newValue = e.target.value;
|
|
||||||
setStoreSearchQuery(newValue);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
value={searchQuery}
|
|
||||||
onInput={handleInput}
|
|
||||||
className="mx-auto max-w-xl focus-visible:ring-0"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { useFormStatus } from "react-dom";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
|
|
||||||
export default function SubmitButton(props: { text: string }) {
|
|
||||||
const { text } = props;
|
|
||||||
const { pending } = useFormStatus();
|
|
||||||
return (
|
|
||||||
<Button disabled={pending}>
|
|
||||||
{pending ? `Adding ${text}` : `Add ${text}`}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
"use server";
|
|
||||||
import { deleteCountry } from "~/server/actions/deleteCountry";
|
|
||||||
import { db } from "~/server/db";
|
|
||||||
|
|
||||||
export default async function WineList() {
|
|
||||||
const wines = await db.query.wines.findMany();
|
|
||||||
return (
|
|
||||||
<div className="pt-4">
|
|
||||||
<h1 className="text-2xl">All wines:</h1>
|
|
||||||
{wines ? (
|
|
||||||
<>
|
|
||||||
<ul>
|
|
||||||
{wines.map((wine) => (
|
|
||||||
<li key={wine.id}>{wine.name}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p>There are no wines in the db.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
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);
|
||||||
|
}
|
|
@ -1,10 +0,0 @@
|
||||||
import CreateCountryForm from "./CreateCountryForm";
|
|
||||||
|
|
||||||
export default function CreateCountry() {
|
|
||||||
return (
|
|
||||||
<div className="container flex w-full flex-col justify-center py-4">
|
|
||||||
<h1 className="pt-4 text-2xl">Fill the form to create a new country</h1>
|
|
||||||
<CreateCountryForm />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import { addCountry } from "~/server/actions/addCountry";
|
|
||||||
import SubmitButton from "../SubmitButton";
|
|
||||||
import { useFormState } from "react-dom";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { Check, CircleX } from "lucide-react";
|
|
||||||
|
|
||||||
export default function CreateCountryForm() {
|
|
||||||
const [formState, formAction] = useFormState(addCountry, {
|
|
||||||
message: "",
|
|
||||||
errors: undefined,
|
|
||||||
fieldValues: {
|
|
||||||
name: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
if (formState.message === "success") {
|
|
||||||
formRef.current?.reset();
|
|
||||||
}
|
|
||||||
}, [formState.message]);
|
|
||||||
return (
|
|
||||||
<form ref={formRef} action={formAction} className="flex flex-col gap-2">
|
|
||||||
<div className="flex max-w-3xl items-center gap-2">
|
|
||||||
<Input
|
|
||||||
name="name"
|
|
||||||
id="name"
|
|
||||||
className={clsx({ "border-red-500": formState.errors?.name })}
|
|
||||||
/>
|
|
||||||
{formState.message !== "" && !formState.errors?.name ? (
|
|
||||||
<Check className="text-green-500" />
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
{formState.errors?.name ? (
|
|
||||||
<div className="flex min-w-96 items-center gap-1 text-red-500">
|
|
||||||
<CircleX />
|
|
||||||
<span className="text-sm">{formState.errors?.name}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<SubmitButton text={"Country"} />
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
"use server";
|
|
||||||
import getAllCountries from "~/server/actions/getAllCountries";
|
|
||||||
import CreateProducerForm from "./CreateProducerForm";
|
|
||||||
|
|
||||||
export default async function CreateProducer() {
|
|
||||||
const allCountries = await getAllCountries();
|
|
||||||
return (
|
|
||||||
<div className="container flex w-full flex-col justify-center py-4">
|
|
||||||
<h1 className="pt-4 text-2xl">Fill the form to create a new producer</h1>
|
|
||||||
<CreateProducerForm allCountries={allCountries} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
"use client";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { Check, CircleX } from "lucide-react";
|
|
||||||
import { useFormState } from "react-dom";
|
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "~/components/ui/select";
|
|
||||||
import { Textarea } from "~/components/ui/textarea";
|
|
||||||
import { addProducer } from "~/server/actions/addProducer";
|
|
||||||
import SubmitButton from "../SubmitButton";
|
|
||||||
|
|
||||||
type Country = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CreateProducerForm(props: { allCountries: Country[] }) {
|
|
||||||
const { allCountries } = props;
|
|
||||||
const [formState, formAction] = useFormState(addProducer, {
|
|
||||||
message: "",
|
|
||||||
errors: undefined,
|
|
||||||
fieldValues: {
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
imageUrl: "",
|
|
||||||
countryId: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<form action={formAction}>
|
|
||||||
<Input
|
|
||||||
name="name"
|
|
||||||
id="name"
|
|
||||||
placeholder="Enter the name of the producer..."
|
|
||||||
className={clsx({ "border-red-500": formState.errors?.name })}
|
|
||||||
/>
|
|
||||||
{formState.message !== "" && !formState.errors?.name ? (
|
|
||||||
<Check className="text-green-500" />
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
{formState.errors?.name ? (
|
|
||||||
<div className="flex min-w-96 items-center gap-1 text-red-500">
|
|
||||||
<CircleX />
|
|
||||||
<span className="text-sm">{formState.errors?.name}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
<Textarea
|
|
||||||
name="description"
|
|
||||||
id="description"
|
|
||||||
placeholder="Enter a description of the producer..."
|
|
||||||
className={clsx({ "border-red-500": formState.errors?.description })}
|
|
||||||
/>
|
|
||||||
{formState.message !== "" && !formState.errors?.description ? (
|
|
||||||
<Check className="text-green-500" />
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
{formState.errors?.description ? (
|
|
||||||
<div className="flex min-w-96 items-center gap-1 text-red-500">
|
|
||||||
<CircleX />
|
|
||||||
<span className="text-sm">{formState.errors?.description}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
<Input
|
|
||||||
name="imageUrl"
|
|
||||||
id="imageUrl"
|
|
||||||
placeholder="Image URL"
|
|
||||||
className={clsx({ "border-red-500": formState.errors?.imageUrl })}
|
|
||||||
/>
|
|
||||||
{formState.message !== "" && !formState.errors?.imageUrl ? (
|
|
||||||
<Check className="text-green-500" />
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
{formState.errors?.imageUrl ? (
|
|
||||||
<div className="flex min-w-96 items-center gap-1 text-red-500">
|
|
||||||
<CircleX />
|
|
||||||
<span className="text-sm">{formState.errors?.imageUrl}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
<Select name="countryId">
|
|
||||||
<SelectTrigger className={`w-[180px] `}>
|
|
||||||
<SelectValue placeholder="select country" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{allCountries.map((country) => (
|
|
||||||
<SelectItem key={country.id} value={country.id}>
|
|
||||||
{country.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<SubmitButton text={"producer"} />
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
"use server";
|
|
||||||
import CreateRegionForm from "./CreateRegionForm";
|
|
||||||
import getAllCountries from "~/server/actions/getAllCountries";
|
|
||||||
|
|
||||||
export default async function CreateRegion() {
|
|
||||||
const allCountries = await getAllCountries();
|
|
||||||
return (
|
|
||||||
<div className="container flex w-full flex-col justify-center py-4">
|
|
||||||
<h1 className="pt-4 text-2xl">Fill the form to create a new region</h1>
|
|
||||||
<CreateRegionForm countries={allCountries} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import SubmitButton from "../SubmitButton";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { addRegion } from "~/server/actions/addRegion";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "~/components/ui/select";
|
|
||||||
import { useFormState } from "react-dom";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { Check, CircleX } from "lucide-react";
|
|
||||||
|
|
||||||
interface Country {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CreateRegionForm(props: { countries: Country[] }) {
|
|
||||||
const [formState, formAction] = useFormState(addRegion, {
|
|
||||||
message: "",
|
|
||||||
errors: undefined,
|
|
||||||
fieldValues: {
|
|
||||||
name: "",
|
|
||||||
countryId: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { countries } = props;
|
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
if (formState.message === "success") {
|
|
||||||
formRef.current?.reset();
|
|
||||||
}
|
|
||||||
}, [formState.message]);
|
|
||||||
return (
|
|
||||||
<form ref={formRef} action={formAction} className="flex flex-col gap-2">
|
|
||||||
<div className="flex max-w-3xl items-center gap-2">
|
|
||||||
<Input
|
|
||||||
name="name"
|
|
||||||
id="name"
|
|
||||||
placeholder="Name"
|
|
||||||
className={`${clsx({ "border-red-500": formState.errors?.name })}`}
|
|
||||||
/>
|
|
||||||
{formState.message === "success" && !formState.errors?.name ? (
|
|
||||||
<Check className="text-green-500" />
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
{formState.errors?.name ? (
|
|
||||||
<div className="flex min-w-40 items-center gap-1 text-red-500">
|
|
||||||
<CircleX className="text-red-500" />
|
|
||||||
<span className="text-sm">{formState.errors?.name}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex max-w-3xl items-center gap-2">
|
|
||||||
<Select name="country">
|
|
||||||
<SelectTrigger
|
|
||||||
className={`w-[180px] ${clsx({ "border-red-500": formState.errors?.countryId })}`}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Country" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{countries.map((country) => (
|
|
||||||
<SelectItem key={country.id} value={country.id}>
|
|
||||||
{country.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{formState.message !== "" && !formState.errors?.countryId ? (
|
|
||||||
<Check className="text-green-500" />
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
{formState.errors?.countryId ? (
|
|
||||||
<CircleX className="text-red-500" />
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SubmitButton text={"Region"} />
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
"use server";
|
|
||||||
import getAllRegions from "~/server/actions/getAllRegions";
|
|
||||||
import CreateSubRegionForm from "./CreateSubRegionForm";
|
|
||||||
import getAllCountries from "~/server/actions/getAllCountries";
|
|
||||||
|
|
||||||
const allRegions = await getAllRegions();
|
|
||||||
const allCountries = await getAllCountries();
|
|
||||||
|
|
||||||
export default async function CreateSubRegion() {
|
|
||||||
return (
|
|
||||||
<div className="container flex w-full flex-col justify-center py-4">
|
|
||||||
<h1 className="pt-4 text-2xl">
|
|
||||||
Fill the form to create a new Sub Region
|
|
||||||
</h1>
|
|
||||||
<CreateSubRegionForm
|
|
||||||
allRegions={allRegions}
|
|
||||||
allCountries={allCountries}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
"use client";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
|
||||||
import { useFormState } from "react-dom";
|
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "~/components/ui/select";
|
|
||||||
import { addSubRegion } from "~/server/actions/addSubRegion";
|
|
||||||
import SubmitButton from "../SubmitButton";
|
|
||||||
import { Check, CircleX } from "lucide-react";
|
|
||||||
|
|
||||||
type Region = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
countryId: string;
|
|
||||||
};
|
|
||||||
type Country = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CreateSubRegionForm(props: {
|
|
||||||
allRegions: Region[];
|
|
||||||
allCountries: Country[];
|
|
||||||
}) {
|
|
||||||
const { allRegions, allCountries } = props;
|
|
||||||
const [selectCountryId, setSelectCountryId] = useState<string | undefined>(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
const [selectCountryRegions, setSelectCountryRegions] = useState<Region[]>(
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const [formState, formActions] = useFormState(addSubRegion, {
|
|
||||||
message: "",
|
|
||||||
errors: undefined,
|
|
||||||
fieldValues: {
|
|
||||||
name: "",
|
|
||||||
regionId: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectCountryId) {
|
|
||||||
const regions = allRegions.filter(
|
|
||||||
(region) => region.countryId === selectCountryId,
|
|
||||||
);
|
|
||||||
setSelectCountryRegions(regions);
|
|
||||||
}
|
|
||||||
}, [selectCountryId]);
|
|
||||||
|
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
if (formState.message === "success") {
|
|
||||||
formRef.current?.reset();
|
|
||||||
}
|
|
||||||
}, [formState.message]);
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{/* country selector */}
|
|
||||||
<Select
|
|
||||||
name="country"
|
|
||||||
onValueChange={(value) => setSelectCountryId(value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className={`w-[180px] `}>
|
|
||||||
<SelectValue placeholder="select country" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{allCountries.map((country) => (
|
|
||||||
<SelectItem key={country.id} value={country.id}>
|
|
||||||
{country.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<form ref={formRef} action={formActions}>
|
|
||||||
{/* region selector */}
|
|
||||||
<div className="flex max-w-3xl items-center gap-2">
|
|
||||||
<Select name="regionId">
|
|
||||||
<SelectTrigger
|
|
||||||
className={`w-[180px] ${clsx({ "border-red-500": formState.errors?.regionId })}`}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="select region" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{selectCountryRegions.map((region) => (
|
|
||||||
<SelectItem key={region.id} value={region.id}>
|
|
||||||
{region.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{formState.message === "success" && !formState.errors?.regionId ? (
|
|
||||||
<Check className="text-green-500" />
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
{formState.errors?.regionId ? (
|
|
||||||
<div className="flex min-w-40 items-center gap-1 text-red-500">
|
|
||||||
<CircleX className="text-red-500" />
|
|
||||||
<span className="text-sm">{formState.errors?.regionId}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex max-w-3xl items-center gap-2">
|
|
||||||
<Input
|
|
||||||
name="name"
|
|
||||||
id="name"
|
|
||||||
placeholder="Name the sub region"
|
|
||||||
className={`${clsx({ "border-red-500": formState.errors?.name })}`}
|
|
||||||
/>
|
|
||||||
{formState.message === "success" && !formState.errors?.name ? (
|
|
||||||
<Check className="text-green-500" />
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
{formState.errors?.name ? (
|
|
||||||
<div className="flex min-w-40 items-center gap-1 text-red-500">
|
|
||||||
<CircleX className="text-red-500" />
|
|
||||||
<span className="text-sm">{formState.errors?.name}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<SubmitButton text={"sub region"} />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { Delete } from "lucide-react";
|
|
||||||
import deleteProducer from "~/server/actions/deleteProducer";
|
|
||||||
import getAllProducers from "~/server/actions/getAllProducers";
|
|
||||||
|
|
||||||
export default async function AllProducers() {
|
|
||||||
const allProducers = await getAllProducers();
|
|
||||||
return (
|
|
||||||
<div className="pt-4">
|
|
||||||
<h1 className="text-2xl">All Producers:</h1>
|
|
||||||
{allProducers.map((producer) => (
|
|
||||||
<div key={producer.id} className="flex gap-1">
|
|
||||||
<p>{producer.name}</p>
|
|
||||||
<form action={deleteProducer.bind(null, producer.id)}>
|
|
||||||
<button>
|
|
||||||
<Delete />
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,15 +1,6 @@
|
||||||
import "~/styles/globals.css";
|
import "~/styles/globals.css";
|
||||||
|
|
||||||
import { Inter as FontSans } from "next/font/google";
|
import { GeistSans } from "geist/font/sans";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import TopNav from "./_components/FilterMenu";
|
|
||||||
import Filtermenu from "./_components/FilterMenu";
|
|
||||||
import SearchBar from "./_components/SearchBar";
|
|
||||||
|
|
||||||
const fontSans = FontSans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-sans",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Create T3 App",
|
title: "Create T3 App",
|
||||||
|
@ -23,19 +14,8 @@ export default function RootLayout({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" className={`${GeistSans.variable}`}>
|
||||||
<body
|
<body>{children}</body>
|
||||||
className={cn(
|
|
||||||
"bg-background min-h-screen font-sans antialiased",
|
|
||||||
fontSans.variable,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="container pt-12">
|
|
||||||
<SearchBar />
|
|
||||||
<Filtermenu />
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import App from "./App";
|
import App from "./_components/App";
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
return (
|
return (
|
||||||
<main className="w-full">
|
<main>
|
||||||
<App />
|
<App />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
163
src/app/store.ts
163
src/app/store.ts
|
@ -1,163 +0,0 @@
|
||||||
import { create } from "zustand";
|
|
||||||
import { produce } from "immer";
|
|
||||||
import { persist, createJSONStorage } from "zustand/middleware";
|
|
||||||
|
|
||||||
export 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 = {
|
|
||||||
setSearchQuery: (searchQuery: string) => void;
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setSearchQuery: (searchQuery: string) =>
|
|
||||||
set(
|
|
||||||
produce((state: FilterState) => {
|
|
||||||
state.filters.searchQuery = searchQuery;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
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;
|
|
|
@ -1,59 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
|
||||||
|
|
||||||
const alertVariants = cva(
|
|
||||||
"relative w-full rounded-lg border border-slate-200 p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-slate-950 dark:border-slate-800 dark:[&>svg]:text-slate-50",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50",
|
|
||||||
destructive:
|
|
||||||
"border-red-500/50 text-red-500 dark:border-red-500 [&>svg]:text-red-500 dark:border-red-900/50 dark:text-red-900 dark:dark:border-red-900 dark:[&>svg]:text-red-900",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
|
||||||
>(({ className, variant, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
role="alert"
|
|
||||||
className={cn(alertVariants({ variant }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Alert.displayName = "Alert"
|
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<h5
|
|
||||||
ref={ref}
|
|
||||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertTitle.displayName = "AlertTitle"
|
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDescription.displayName = "AlertDescription"
|
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
|
|
@ -1,56 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-slate-900 text-slate-50 hover:bg-slate-900/90 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/90",
|
|
||||||
destructive:
|
|
||||||
"bg-red-500 text-slate-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-slate-50 dark:hover:bg-red-900/90",
|
|
||||||
outline:
|
|
||||||
"border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50",
|
|
||||||
secondary:
|
|
||||||
"bg-slate-100 text-slate-900 hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80",
|
|
||||||
ghost: "hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50",
|
|
||||||
link: "text-slate-900 underline-offset-4 hover:underline dark:text-slate-50",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-10 px-4 py-2",
|
|
||||||
sm: "h-9 rounded-md px-3",
|
|
||||||
lg: "h-11 rounded-md px-8",
|
|
||||||
icon: "h-10 w-10",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export interface ButtonProps
|
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
||||||
VariantProps<typeof buttonVariants> {
|
|
||||||
asChild?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Button.displayName = "Button"
|
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
|
|
@ -1,25 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
|
||||||
|
|
||||||
export interface InputProps
|
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
||||||
({ className, type, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={type}
|
|
||||||
className={cn(
|
|
||||||
"flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:bg-slate-950 dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus-visible:ring-slate-300",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Input.displayName = "Input"
|
|
||||||
|
|
||||||
export { Input }
|
|
|
@ -1,128 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
|
||||||
import { cva } from "class-variance-authority"
|
|
||||||
import { ChevronDown } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
|
||||||
|
|
||||||
const NavigationMenu = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<NavigationMenuViewport />
|
|
||||||
</NavigationMenuPrimitive.Root>
|
|
||||||
))
|
|
||||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const NavigationMenuList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
|
||||||
|
|
||||||
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
|
||||||
|
|
||||||
const navigationMenuTriggerStyle = cva(
|
|
||||||
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-white px-4 py-2 text-sm font-medium transition-colors hover:bg-slate-100 hover:text-slate-900 focus:bg-slate-100 focus:text-slate-900 focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-slate-100/50 data-[state=open]:bg-slate-100/50 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50 dark:focus:bg-slate-800 dark:focus:text-slate-50 dark:data-[active]:bg-slate-800/50 dark:data-[state=open]:bg-slate-800/50"
|
|
||||||
)
|
|
||||||
|
|
||||||
const NavigationMenuTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}{""}
|
|
||||||
<ChevronDown
|
|
||||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</NavigationMenuPrimitive.Trigger>
|
|
||||||
))
|
|
||||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const NavigationMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
|
||||||
|
|
||||||
const NavigationMenuViewport = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
|
||||||
<NavigationMenuPrimitive.Viewport
|
|
||||||
className={cn(
|
|
||||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border border-slate-200 bg-white text-slate-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)] dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
NavigationMenuViewport.displayName =
|
|
||||||
NavigationMenuPrimitive.Viewport.displayName
|
|
||||||
|
|
||||||
const NavigationMenuIndicator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Indicator
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-slate-200 shadow-md dark:bg-slate-800" />
|
|
||||||
</NavigationMenuPrimitive.Indicator>
|
|
||||||
))
|
|
||||||
NavigationMenuIndicator.displayName =
|
|
||||||
NavigationMenuPrimitive.Indicator.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
navigationMenuTriggerStyle,
|
|
||||||
NavigationMenu,
|
|
||||||
NavigationMenuList,
|
|
||||||
NavigationMenuItem,
|
|
||||||
NavigationMenuContent,
|
|
||||||
NavigationMenuTrigger,
|
|
||||||
NavigationMenuLink,
|
|
||||||
NavigationMenuIndicator,
|
|
||||||
NavigationMenuViewport,
|
|
||||||
}
|
|
|
@ -1,160 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
||||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root
|
|
||||||
|
|
||||||
const SelectGroup = SelectPrimitive.Group
|
|
||||||
|
|
||||||
const SelectValue = SelectPrimitive.Value
|
|
||||||
|
|
||||||
const SelectTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-10 w-full items-center justify-between rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 dark:border-slate-800 dark:bg-slate-950 dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus:ring-slate-300",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon asChild>
|
|
||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
))
|
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronUp className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
))
|
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
))
|
|
||||||
SelectScrollDownButton.displayName =
|
|
||||||
SelectPrimitive.ScrollDownButton.displayName
|
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
||||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white text-slate-950 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
|
|
||||||
position === "popper" &&
|
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
position={position}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SelectScrollUpButton />
|
|
||||||
<SelectPrimitive.Viewport
|
|
||||||
className={cn(
|
|
||||||
"p-1",
|
|
||||||
position === "popper" &&
|
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
<SelectScrollDownButton />
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
))
|
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<SelectPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
))
|
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-800", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
Select,
|
|
||||||
SelectGroup,
|
|
||||||
SelectValue,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
|
||||||
SelectLabel,
|
|
||||||
SelectItem,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectScrollDownButton,
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
|
|
||||||
const SliderTwoThumbs = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SliderPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-full touch-none select-none items-center",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800">
|
|
||||||
<SliderPrimitive.Range className="absolute h-full bg-slate-900 dark:bg-slate-50" />
|
|
||||||
</SliderPrimitive.Track>
|
|
||||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-slate-900 bg-white ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-50 dark:bg-slate-950 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300" />
|
|
||||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-slate-900 bg-white ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-50 dark:bg-slate-950 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300" />
|
|
||||||
</SliderPrimitive.Root>
|
|
||||||
));
|
|
||||||
SliderTwoThumbs.displayName = SliderPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { SliderTwoThumbs };
|
|
|
@ -1,24 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
|
||||||
|
|
||||||
export interface TextareaProps
|
|
||||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
|
||||||
|
|
||||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
||||||
({ className, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<textarea
|
|
||||||
className={cn(
|
|
||||||
"flex min-h-[80px] w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:bg-slate-950 dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus-visible:ring-slate-300",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Textarea.displayName = "Textarea"
|
|
||||||
|
|
||||||
export { Textarea }
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { type ClassValue, clsx } from "clsx"
|
|
||||||
import { twMerge } from "tailwind-merge"
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
|
||||||
return twMerge(clsx(inputs))
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { db } from "../db";
|
|
||||||
import { countries } from "../db/schema";
|
|
||||||
import { ZodError, z } from "zod";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export async function addCountry(prevstate: any, formData: FormData) {
|
|
||||||
//assign formdaata to variables.
|
|
||||||
const name = (formData.get("name") as string).toLowerCase();
|
|
||||||
//check if country already exists
|
|
||||||
const exists = await db
|
|
||||||
.select({ name: countries.name })
|
|
||||||
.from(countries)
|
|
||||||
.where(eq(countries.name, name));
|
|
||||||
|
|
||||||
//Define the schema for the form data
|
|
||||||
const schema = z.object({
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "Name is required" })
|
|
||||||
.refine(() => !exists[0], { message: `${name} already exists` }),
|
|
||||||
});
|
|
||||||
//Parse the form data using the schema for validation, and check if the name already exists
|
|
||||||
try {
|
|
||||||
schema.parse({
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
//If the name doesn't exist, add the country to the database abd revalidate the page
|
|
||||||
await db.insert(countries).values({ name });
|
|
||||||
revalidatePath("/");
|
|
||||||
//Return a success message
|
|
||||||
return {
|
|
||||||
message: "success",
|
|
||||||
errors: undefined,
|
|
||||||
fieldValues: {
|
|
||||||
name: "",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const zodError = error as ZodError;
|
|
||||||
const errorMap = zodError.flatten().fieldErrors;
|
|
||||||
//Return an error object with the field values and errors.
|
|
||||||
return {
|
|
||||||
message: "error",
|
|
||||||
errors: {
|
|
||||||
name: errorMap["name"]?.[0] ?? "",
|
|
||||||
},
|
|
||||||
fieldValues: {
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
"use server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { db } from "../db";
|
|
||||||
import { producers } from "../db/schema";
|
|
||||||
import { ZodError, z } from "zod";
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
|
|
||||||
export const addProducer = async (prevstate: any, formData: FormData) => {
|
|
||||||
//assign formdaata to variables.
|
|
||||||
const name = (formData.get("name") as string).toLowerCase();
|
|
||||||
const description = (formData.get("description") as string).toLowerCase();
|
|
||||||
const imageUrl = (formData.get("imageUrl") as string).toLowerCase();
|
|
||||||
const countryId = formData.get("countryId") as string;
|
|
||||||
|
|
||||||
//check if producer already exists in country
|
|
||||||
const exists = await db
|
|
||||||
.select({ name: producers.name })
|
|
||||||
.from(producers)
|
|
||||||
.where(eq(producers.name, name));
|
|
||||||
|
|
||||||
//Define the schema for the form data
|
|
||||||
const schema = z.object({
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.min(1, "Name is required")
|
|
||||||
.refine(() => !exists[0], {
|
|
||||||
message: `${name} already exists in selected country`,
|
|
||||||
}),
|
|
||||||
description: z.string(),
|
|
||||||
imageUrl: z.string(),
|
|
||||||
countryId: z.string().min(1, "No country selected"),
|
|
||||||
});
|
|
||||||
//Parse the form data using the schema for validation, and check if the name already exists
|
|
||||||
try {
|
|
||||||
schema.parse({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
imageUrl,
|
|
||||||
countryId,
|
|
||||||
});
|
|
||||||
//If the name doesn't exist, add the country to the database abd revalidate the page
|
|
||||||
await db
|
|
||||||
.insert(producers)
|
|
||||||
.values({ name, description, imageUrl, countryId });
|
|
||||||
revalidatePath("/");
|
|
||||||
//Return a success message
|
|
||||||
return {
|
|
||||||
message: "success",
|
|
||||||
errors: undefined,
|
|
||||||
fieldValues: {
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
imageUrl: "",
|
|
||||||
countryId: "",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const zodError = error as ZodError;
|
|
||||||
const errorMap = zodError.flatten().fieldErrors;
|
|
||||||
//Return an error object with the field values and errors.
|
|
||||||
return {
|
|
||||||
message: "error",
|
|
||||||
errors: {
|
|
||||||
name: errorMap["name"]?.[0] ?? "",
|
|
||||||
description: errorMap["description"]?.[0] ?? "",
|
|
||||||
imageUrl: errorMap["imageUrl"]?.[0] ?? "",
|
|
||||||
countryId: errorMap["countryId"]?.[0] ?? "",
|
|
||||||
},
|
|
||||||
fieldValues: {
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
imageUrl,
|
|
||||||
countryId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,65 +0,0 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { db } from "../db";
|
|
||||||
import { regions } from "../db/schema";
|
|
||||||
import { ZodError, z } from "zod";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export const addRegion = async (prevstate: any, formData: FormData) => {
|
|
||||||
//assign formdaata to variables.
|
|
||||||
const name = (formData.get("name") as string).toLowerCase();
|
|
||||||
const countryId = formData.get("country") as string;
|
|
||||||
|
|
||||||
//check if region already exists in country
|
|
||||||
const exists = await db
|
|
||||||
.select({ name: regions.name })
|
|
||||||
.from(regions)
|
|
||||||
.where(eq(regions.countryId, countryId) && eq(regions.name, name));
|
|
||||||
|
|
||||||
//Define the schema for the form data
|
|
||||||
const schema = z.object({
|
|
||||||
countryId: z.string().min(1, "No country selected"),
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.min(1, "Name is required")
|
|
||||||
.refine(() => !exists[0], {
|
|
||||||
message: `${name} already exists in selected country`,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
//Parse the form data using the schema for validation, and check if the name already exists
|
|
||||||
try {
|
|
||||||
schema.parse({
|
|
||||||
countryId,
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
//If the name doesn't exist, add the country to the database abd revalidate the page
|
|
||||||
await db.insert(regions).values({ countryId, name });
|
|
||||||
revalidatePath("/");
|
|
||||||
//Return a success message
|
|
||||||
return {
|
|
||||||
message: "success",
|
|
||||||
errors: undefined,
|
|
||||||
fieldValues: {
|
|
||||||
name: "",
|
|
||||||
countryId: "",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const zodError = error as ZodError;
|
|
||||||
const errorMap = zodError.flatten().fieldErrors;
|
|
||||||
//Return an error object with the field values and errors.
|
|
||||||
return {
|
|
||||||
message: "error",
|
|
||||||
errors: {
|
|
||||||
name: errorMap["name"]?.[0] ?? "",
|
|
||||||
countryId: errorMap["countryId"]?.[0] ?? "",
|
|
||||||
},
|
|
||||||
fieldValues: {
|
|
||||||
name,
|
|
||||||
countryId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,64 +0,0 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { db } from "../db";
|
|
||||||
import { subRegions } from "../db/schema";
|
|
||||||
import { ZodError, z } from "zod";
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
|
|
||||||
export const addSubRegion = async (prevstate: any, formData: FormData) => {
|
|
||||||
//assign formdaata to variables.
|
|
||||||
const name = (formData.get("name") as string).toLowerCase();
|
|
||||||
const regionId = formData.get("regionId") as string;
|
|
||||||
|
|
||||||
//check if region already exists in country
|
|
||||||
const exists = await db
|
|
||||||
.select({ name: subRegions.name })
|
|
||||||
.from(subRegions)
|
|
||||||
.where(eq(subRegions.regionId, regionId) && eq(subRegions.name, name));
|
|
||||||
|
|
||||||
//Define the schema for the form data
|
|
||||||
const schema = z.object({
|
|
||||||
regionId: z.string().min(1, "No region selected"),
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.min(1, "Name is required")
|
|
||||||
.refine(() => !exists[0], {
|
|
||||||
message: `${name} already exists in selected region`,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
//Parse the form data using the schema for validation, and check if the name already exists
|
|
||||||
try {
|
|
||||||
schema.parse({
|
|
||||||
regionId,
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
//If the name doesn't exist, add the country to the database abd revalidate the page
|
|
||||||
await db.insert(subRegions).values({ regionId, name });
|
|
||||||
revalidatePath("/");
|
|
||||||
//Return a success message
|
|
||||||
return {
|
|
||||||
message: "success",
|
|
||||||
errors: undefined,
|
|
||||||
fieldValues: {
|
|
||||||
name: "",
|
|
||||||
regionId: "",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const zodError = error as ZodError;
|
|
||||||
const errorMap = zodError.flatten().fieldErrors;
|
|
||||||
//Return an error object with the field values and errors.
|
|
||||||
return {
|
|
||||||
message: "error",
|
|
||||||
errors: {
|
|
||||||
name: errorMap["name"]?.[0] ?? "",
|
|
||||||
regionId: errorMap["regionId"]?.[0] ?? "",
|
|
||||||
},
|
|
||||||
fieldValues: {
|
|
||||||
name,
|
|
||||||
regionId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
67
src/server/actions/addWine.ts
Normal file
67
src/server/actions/addWine.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
'use server'
|
||||||
|
import { ZodError, z } from 'zod';
|
||||||
|
import { db } from '../db/index'
|
||||||
|
import { wines } from '../db/schema'
|
||||||
|
import { QueryResult } from 'pg';
|
||||||
|
|
||||||
|
type NewWine = typeof wines.$inferInsert;
|
||||||
|
|
||||||
|
export type InsertResult = {
|
||||||
|
name: string;
|
||||||
|
producer: string;
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date | null;
|
||||||
|
|
||||||
|
}[];
|
||||||
|
export type Fields = {
|
||||||
|
name: FormDataEntryValue | null
|
||||||
|
producer: FormDataEntryValue | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormState = {
|
||||||
|
message: string | QueryResult<never>;
|
||||||
|
errors: Record<keyof Fields, string> | undefined;
|
||||||
|
fieldValues: NewWine;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
producer: z.string().uuid(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const addWine = async (
|
||||||
|
prevState: FormState,
|
||||||
|
formData: FormData): Promise<FormState> => {
|
||||||
|
const newWine: NewWine = {
|
||||||
|
name: formData.get('name') as string,
|
||||||
|
producer: formData.get('producer') as string
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
schema.parse(newWine)
|
||||||
|
|
||||||
|
await db.insert(wines).values(newWine);
|
||||||
|
return {
|
||||||
|
message: 'success',
|
||||||
|
errors: undefined,
|
||||||
|
fieldValues: {
|
||||||
|
name: "",
|
||||||
|
producer: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const zodError = error as ZodError;
|
||||||
|
const errorMap = zodError.flatten().fieldErrors;
|
||||||
|
return {
|
||||||
|
message: "error",
|
||||||
|
errors: {
|
||||||
|
name: errorMap["name"]?.[0] ?? "",
|
||||||
|
producer: errorMap["producer"]?.[0] ?? ""
|
||||||
|
},
|
||||||
|
fieldValues: {
|
||||||
|
name: newWine.name,
|
||||||
|
producer: newWine.producer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
src/server/actions/allProducers.ts
Normal file
7
src/server/actions/allProducers.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
'use server'
|
||||||
|
import { db } from "../db/index";
|
||||||
|
|
||||||
|
|
||||||
|
export async function getProducers(){
|
||||||
|
return db.query.producers.findMany();
|
||||||
|
}
|
7
src/server/actions/allWines.ts
Normal file
7
src/server/actions/allWines.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
'use server'
|
||||||
|
import { db } from "../db/index";
|
||||||
|
|
||||||
|
|
||||||
|
export async function getWines(){
|
||||||
|
return db.query.wines.findMany();
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { db } from "../db";
|
|
||||||
import { countries } from "../db/schema";
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
|
|
||||||
export async function deleteCountry(id: string) {
|
|
||||||
await db.delete(countries).where(eq(countries.id, id));
|
|
||||||
revalidatePath("/");
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
"use server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { db } from "../db";
|
|
||||||
import { producers } from "../db/schema";
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
|
|
||||||
export default async function deleteProducer(id: string) {
|
|
||||||
await db.delete(producers).where(eq(producers.id, id));
|
|
||||||
revalidatePath("/");
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
"use server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { db } from "../db";
|
|
||||||
import { regions } from "../db/schema";
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
|
|
||||||
export default async function deleteRegion(id: string) {
|
|
||||||
await db.delete(regions).where(eq(regions.id, id));
|
|
||||||
revalidatePath("/");
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
"use server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { db } from "../db";
|
|
||||||
import { subRegions } from "../db/schema";
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
|
|
||||||
export default async function deleteSubRegion(id: string) {
|
|
||||||
await db.delete(subRegions).where(eq(subRegions.id, id));
|
|
||||||
revalidatePath("/");
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { db } from "../db";
|
|
||||||
|
|
||||||
export default async function getAllCountries() {
|
|
||||||
const allCountries = await db.query.countries.findMany();
|
|
||||||
return allCountries;
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
"use server";
|
|
||||||
import { db } from "../db";
|
|
||||||
|
|
||||||
export default async function getAllProducers() {
|
|
||||||
const allProducers = await db.query.producers.findMany();
|
|
||||||
return allProducers;
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { db } from "../db";
|
|
||||||
|
|
||||||
export default async function getAllRegions() {
|
|
||||||
const allRegions = await db.query.regions.findMany();
|
|
||||||
return allRegions;
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
"use server";
|
|
||||||
import { db } from "../db";
|
|
||||||
|
|
||||||
export default async function getAllSubRegions() {
|
|
||||||
const allSubRegions = await db.query.subRegions.findMany();
|
|
||||||
return allSubRegions;
|
|
||||||
}
|
|
10
src/server/actions/getProducer.ts
Normal file
10
src/server/actions/getProducer.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
"use server";
|
||||||
|
import { producers } from "../db/schema";
|
||||||
|
import { db } from "../db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export default async function getProducer(id: string) {
|
||||||
|
return db.query.producers.findFirst({
|
||||||
|
where: eq(producers.id, id),
|
||||||
|
});
|
||||||
|
}
|
13
src/server/actions/getWineDetails.ts
Normal file
13
src/server/actions/getWineDetails.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
"use server";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import { db } from "../db";
|
||||||
|
import { wines, producers } from "../db/schema";
|
||||||
|
import { UUID } from "crypto";
|
||||||
|
|
||||||
|
export async function getWineDetails(id: string) {
|
||||||
|
const results = await db
|
||||||
|
.select()
|
||||||
|
.from(producers)
|
||||||
|
.where(sql`${producers.id} = ${id}`);
|
||||||
|
return results;
|
||||||
|
}
|
|
@ -1,142 +1,59 @@
|
||||||
import {
|
import {
|
||||||
boolean,
|
|
||||||
index,
|
|
||||||
integer,
|
|
||||||
pgEnum,
|
|
||||||
pgTableCreator,
|
pgTableCreator,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
uniqueIndex,
|
||||||
uuid,
|
uuid,
|
||||||
unique,
|
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
|
|
||||||
export const createTable = pgTableCreator((name) => `wine-shop_${name}`);
|
export const createTable = pgTableCreator((name) => `wine-shop_${name}`);
|
||||||
|
|
||||||
// Producer Schema
|
|
||||||
export const producers = createTable(
|
export const producers = createTable(
|
||||||
"producer",
|
"producer",
|
||||||
{
|
{
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
name: text("name").notNull().unique(),
|
name: text("name").notNull(),
|
||||||
description: text("description"),
|
|
||||||
imageUrl: text("imageUrl"),
|
|
||||||
countryId: uuid("countryId")
|
|
||||||
.references(() => countries.id, { onDelete: "cascade" })
|
|
||||||
.notNull(),
|
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updatedAt").defaultNow(),
|
country: text("country").notNull(),
|
||||||
|
region: text("region").notNull(),
|
||||||
|
email: text("email").notNull(),
|
||||||
},
|
},
|
||||||
(producers) => {
|
(producers) => {
|
||||||
return {
|
return {
|
||||||
nameIdx: index("producer_name").on(producers.name),
|
uniqueIdx: uniqueIndex("email").on(producers.email),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// one-to-many relation producer -> wines
|
|
||||||
export const producersRelations = relations(producers, ({ many }) => ({
|
export const producersRelations = relations(producers, ({ many }) => ({
|
||||||
wines: many(wines),
|
wines: many(wines),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Wines schema
|
export const wines = createTable(
|
||||||
export const typeEnum = pgEnum("type", [
|
"wine",
|
||||||
"sparkling",
|
{
|
||||||
"white",
|
|
||||||
"red",
|
|
||||||
"sweet",
|
|
||||||
"other",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const wines = createTable("wine", {
|
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
type: typeEnum("type").notNull(),
|
|
||||||
description: text("description"),
|
|
||||||
imageUrl: text("imageUrl"),
|
|
||||||
producerId: uuid("producerId")
|
|
||||||
.references(() => producers.id, { onDelete: "cascade" })
|
|
||||||
.notNull(),
|
|
||||||
subRegionId: uuid("subRegionId"),
|
|
||||||
regionId: uuid("regionId").notNull(),
|
|
||||||
countryId: uuid("countryId").notNull(),
|
|
||||||
price: integer("price").notNull(),
|
|
||||||
inStock: boolean("inStock").notNull().default(false),
|
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updatedAt").defaultNow(),
|
updatedAt: timestamp("updatedAt").defaultNow(),
|
||||||
});
|
producer: uuid("producer").notNull(),
|
||||||
|
},
|
||||||
|
(wines) => {
|
||||||
|
return {
|
||||||
|
uniqueIdx: uniqueIndex("unique_idx").on(wines.id),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// many-to-one relationship wine -> producer
|
|
||||||
export const winesRelations = relations(wines, ({ one }) => ({
|
export const winesRelations = relations(wines, ({ one }) => ({
|
||||||
producer: one(producers, {
|
producer: one(producers, {
|
||||||
fields: [wines.producerId],
|
fields: [wines.producer],
|
||||||
references: [producers.id],
|
references: [producers.id],
|
||||||
}),
|
}),
|
||||||
subregion: one(subRegions, {
|
|
||||||
fields: [wines.subRegionId],
|
|
||||||
references: [subRegions.id],
|
|
||||||
}),
|
|
||||||
region: one(regions, {
|
|
||||||
fields: [wines.regionId],
|
|
||||||
references: [regions.id],
|
|
||||||
}),
|
|
||||||
country: one(countries, {
|
|
||||||
fields: [wines.countryId],
|
|
||||||
references: [countries.id],
|
|
||||||
}),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const subRegions = createTable(
|
export type SelectWine = typeof wines.$inferSelect;
|
||||||
"subRegion",
|
|
||||||
{
|
|
||||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
|
||||||
name: text("name").notNull().unique(),
|
|
||||||
regionId: uuid("regionId")
|
|
||||||
.references(() => regions.id, { onDelete: "cascade" })
|
|
||||||
.notNull(),
|
|
||||||
},
|
|
||||||
(t) => ({
|
|
||||||
uniqueSubRegion: unique().on(t.name, t.regionId),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const subRegionsRelations = relations(subRegions, ({ many, one }) => ({
|
export type SelectProducer = typeof producers.$inferSelect;
|
||||||
wines: many(wines),
|
export type InsertProducer = typeof producers.$inferInsert;
|
||||||
region: one(regions, {
|
|
||||||
fields: [subRegions.regionId],
|
|
||||||
references: [regions.id],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const regions = createTable(
|
|
||||||
"region",
|
|
||||||
{
|
|
||||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
|
||||||
name: text("name").notNull(),
|
|
||||||
countryId: uuid("countryId")
|
|
||||||
.references(() => countries.id, { onDelete: "cascade" })
|
|
||||||
.notNull(),
|
|
||||||
},
|
|
||||||
(t) => ({
|
|
||||||
uniqueRegion: unique().on(t.name, t.countryId),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const regionsRelations = relations(regions, ({ many, one }) => ({
|
|
||||||
wines: many(wines),
|
|
||||||
subRegions: many(subRegions),
|
|
||||||
country: one(countries, {
|
|
||||||
fields: [regions.countryId],
|
|
||||||
references: [countries.id],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const countries = createTable("country", {
|
|
||||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
|
||||||
name: text("name").notNull().unique(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const countriesRelations = relations(countries, ({ many }) => ({
|
|
||||||
wines: many(wines),
|
|
||||||
regions: many(regions),
|
|
||||||
}));
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
|
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;
|
||||||
|
}
|
|
@ -1,40 +1,9 @@
|
||||||
import type { Config } from "tailwindcss"
|
import { type Config } from "tailwindcss";
|
||||||
|
|
||||||
const config = {
|
export default {
|
||||||
darkMode: ["class"],
|
content: ["./src/**/*.tsx"],
|
||||||
content: [
|
|
||||||
'./pages/**/*.{ts,tsx}',
|
|
||||||
'./components/**/*.{ts,tsx}',
|
|
||||||
'./app/**/*.{ts,tsx}',
|
|
||||||
'./src/**/*.{ts,tsx}',
|
|
||||||
],
|
|
||||||
prefix: "",
|
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
extend: {},
|
||||||
center: true,
|
|
||||||
padding: "2rem",
|
|
||||||
screens: {
|
|
||||||
"2xl": "1400px",
|
|
||||||
},
|
},
|
||||||
},
|
plugins: [],
|
||||||
extend: {
|
} satisfies Config;
|
||||||
keyframes: {
|
|
||||||
"accordion-down": {
|
|
||||||
from: { height: "0" },
|
|
||||||
to: { height: "var(--radix-accordion-content-height)" },
|
|
||||||
},
|
|
||||||
"accordion-up": {
|
|
||||||
from: { height: "var(--radix-accordion-content-height)" },
|
|
||||||
to: { height: "0" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [require("tailwindcss-animate")],
|
|
||||||
} satisfies Config
|
|
||||||
|
|
||||||
export default config
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user