Compare commits

..

1 Commits
main ... nextui

Author SHA1 Message Date
6137a166eb nextui lib
All checks were successful
Vercel Preview Deployment / Deploy-Preview (push) Successful in 2m39s
2024-05-25 12:25:01 +02:00
59 changed files with 3730 additions and 2741 deletions

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -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>
);
}

View File

@ -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)}
</>
);
}

View 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>
);
}

View File

@ -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"
/>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

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

View 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);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
); );
} }

View File

@ -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>
); );

View File

@ -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;

View File

@ -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 }

View File

@ -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 }

View File

@ -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 }

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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 };

View File

@ -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 }

View File

@ -1,6 +0,0 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -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,
},
};
}
}

View File

@ -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,
},
};
}
};

View File

@ -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,
},
};
}
};

View File

@ -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,
},
};
}
};

View 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
}
}
}
}

View File

@ -0,0 +1,7 @@
'use server'
import { db } from "../db/index";
export async function getProducers(){
return db.query.producers.findMany();
}

View File

@ -0,0 +1,7 @@
'use server'
import { db } from "../db/index";
export async function getWines(){
return db.query.wines.findMany();
}

View File

@ -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("/");
}

View File

@ -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("/");
}

View File

@ -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("/");
}

View File

@ -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("/");
}

View File

@ -1,6 +0,0 @@
import { db } from "../db";
export default async function getAllCountries() {
const allCountries = await db.query.countries.findMany();
return allCountries;
}

View File

@ -1,7 +0,0 @@
"use server";
import { db } from "../db";
export default async function getAllProducers() {
const allProducers = await db.query.producers.findMany();
return allProducers;
}

View File

@ -1,6 +0,0 @@
import { db } from "../db";
export default async function getAllRegions() {
const allRegions = await db.query.regions.findMany();
return allRegions;
}

View File

@ -1,7 +0,0 @@
"use server";
import { db } from "../db";
export default async function getAllSubRegions() {
const allSubRegions = await db.query.subRegions.findMany();
return allSubRegions;
}

View 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),
});
}

View 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;
}

View File

@ -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", id: uuid("id").primaryKey().defaultRandom(),
"red", name: text("name").notNull(),
"sweet", createdAt: timestamp("createdAt").defaultNow().notNull(),
"other", updatedAt: timestamp("updatedAt").defaultNow(),
]); producer: uuid("producer").notNull(),
},
(wines) => {
return {
uniqueIdx: uniqueIndex("unique_idx").on(wines.id),
};
},
);
export const wines = createTable("wine", {
id: uuid("id").primaryKey().defaultRandom(),
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(),
updatedAt: timestamp("updatedAt").defaultNow(),
});
// 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),
}));

View File

@ -1,4 +1,3 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

8
src/types/types.ts Normal file
View File

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

View File

@ -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",
},
},
extend: {
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")], plugins: [],
} satisfies Config } satisfies Config;
export default config