Compare commits

...

5 Commits

Author SHA1 Message Date
93c0348564 Merge pull request 'admin-forms' (#12) from admin-forms into main
All checks were successful
Vercel Production Deployment / Deploy-Production (push) Successful in 1m20s
Reviewed-on: #12
2024-06-04 14:03:20 +00:00
14fa130ce6 added forms for adding producer.
All checks were successful
Vercel Preview Deployment / Deploy-Preview (push) Successful in 1m19s
added delete button to all categories.
2024-06-04 16:02:29 +02:00
a609300688 country selector working now.
All checks were successful
Vercel Preview Deployment / Deploy-Preview (push) Successful in 1m17s
2024-06-04 14:37:47 +02:00
7304ce1023 CreateSubRegionForm logic unfinished
All checks were successful
Vercel Preview Deployment / Deploy-Preview (push) Successful in 1m16s
2024-06-03 15:28:22 +02:00
4b554cdb37 refactored orm operations in components to server actions
All checks were successful
Vercel Preview Deployment / Deploy-Preview (push) Successful in 1m13s
2024-06-03 13:38:20 +02:00
21 changed files with 575 additions and 29 deletions

View File

@ -4,6 +4,10 @@ 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 (
@ -14,6 +18,10 @@ export default function App() {
<AllCountries />
<CreateRegion />
<AllRegions />
<CreateSubRegion />
<AllSubRegions />
<CreateProducer />
<AllProducers />
</div>
);
}

View File

@ -1,9 +1,9 @@
import { Delete } from "lucide-react";
import { deleteCountry } from "~/server/actions/deleteCountry";
import { db } from "~/server/db";
import getAllCountries from "~/server/actions/getAllCountries";
export default async function AllCountries() {
const countries = await db.query.countries.findMany();
const countries = await getAllCountries();
return (
<div className="pt-4">
<h1 className="text-2xl">All Countries:</h1>

View File

@ -1,29 +1,24 @@
import { db } from "~/server/db";
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 db.query.regions.findMany();
const allCountries = await db.query.countries.findMany();
const allRegions = await getAllRegions();
const allCountries = await getAllCountries();
return (
<div className="pt-4">
<h1 className="text-2xl">All Regions:</h1>
{allRegions[0] ? (
<>
<ul>
{allRegions.map((region) => (
<li key={region.id}>
{region.name} -{" "}
{
allCountries.find(
(country) => country.id === region.countryId,
)?.name
}
</li>
))}
</ul>
</>
) : (
<p>There are no regions in the db.</p>
)}
{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

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

@ -2,9 +2,9 @@ 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

@ -0,0 +1,13 @@
"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

@ -0,0 +1,108 @@
"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 +1,13 @@
"use server";
import { db } from "~/server/db";
import CreateRegionForm from "./CreateRegionForm";
import getAllCountries from "~/server/actions/getAllCountries";
export default async function CreateRegion() {
const allCountries = await db.query.countries.findMany();
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

@ -0,0 +1,21 @@
"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

@ -0,0 +1,136 @@
"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

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

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

@ -0,0 +1,77 @@
"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

@ -0,0 +1,64 @@
"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,10 @@
"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

@ -0,0 +1,10 @@
"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

@ -0,0 +1,10 @@
"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

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

View File

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

View File

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

View File

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