styles, form, backend partial
All checks were successful
Lint / Lint (push) Successful in 1m8s

This commit is contained in:
Felix Schulze 2024-10-18 13:12:19 +02:00
parent 0406e1daf4
commit 4db42044ce
11 changed files with 530 additions and 71 deletions

View File

@ -25,51 +25,52 @@
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 240 10% 3.9%; --foreground: 20 14.3% 4.1%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 240 10% 3.9%; --card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%; --popover-foreground: 20 14.3% 4.1%;
--primary: 240 5.9% 10%; --primary: 24.6 95% 53.1%;
--primary-foreground: 0 0% 98%; --primary-foreground: 60 9.1% 97.8%;
--secondary: 240 4.8% 95.9%; --secondary: 60 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%; --secondary-foreground: 24 9.8% 10%;
--muted: 240 4.8% 95.9%; --muted: 60 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%; --muted-foreground: 25 5.3% 44.7%;
--accent: 240 4.8% 95.9%; --accent: 60 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%; --accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 60 9.1% 97.8%;
--border: 240 5.9% 90%; --border: 20 5.9% 90%;
--input: 240 5.9% 90%; --input: 20 5.9% 90%;
--ring: 240 10% 3.9%; --ring: 24.6 95% 53.1%;
--radius: 1rem;
--chart-1: 12 76% 61%; --chart-1: 12 76% 61%;
--chart-2: 173 58% 39%; --chart-2: 173 58% 39%;
--chart-3: 197 37% 24%; --chart-3: 197 37% 24%;
--chart-4: 43 74% 66%; --chart-4: 43 74% 66%;
--chart-5: 27 87% 67%; --chart-5: 27 87% 67%;
--radius: 0.5rem;
} }
.dark { .dark {
--background: 240 10% 3.9%; --background: 20 14.3% 4.1%;
--foreground: 0 0% 98%; --foreground: 60 9.1% 97.8%;
--card: 240 10% 3.9%; --card: 20 14.3% 4.1%;
--card-foreground: 0 0% 98%; --card-foreground: 60 9.1% 97.8%;
--popover: 240 10% 3.9%; --popover: 20 14.3% 4.1%;
--popover-foreground: 0 0% 98%; --popover-foreground: 60 9.1% 97.8%;
--primary: 0 0% 98%; --primary: 20.5 90.2% 48.2%;
--primary-foreground: 240 5.9% 10%; --primary-foreground: 60 9.1% 97.8%;
--secondary: 240 3.7% 15.9%; --secondary: 12 6.5% 15.1%;
--secondary-foreground: 0 0% 98%; --secondary-foreground: 60 9.1% 97.8%;
--muted: 240 3.7% 15.9%; --muted: 12 6.5% 15.1%;
--muted-foreground: 240 5% 64.9%; --muted-foreground: 24 5.4% 63.9%;
--accent: 240 3.7% 15.9%; --accent: 12 6.5% 15.1%;
--accent-foreground: 0 0% 98%; --accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%; --destructive: 0 72.2% 50.6%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 60 9.1% 97.8%;
--border: 240 3.7% 15.9%; --border: 12 6.5% 15.1%;
--input: 240 3.7% 15.9%; --input: 12 6.5% 15.1%;
--ring: 240 4.9% 83.9%; --ring: 20.5 90.2% 48.2%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;
--chart-3: 30 80% 55%; --chart-3: 30 80% 55%;

View File

@ -20,7 +20,7 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en" className="scroll-smooth"> <html lang="en" className="scroll-smooth dark">
<head> <head>
<PlausibleProvider <PlausibleProvider
domain="bangersandmashgbg.com" domain="bangersandmashgbg.com"

View File

@ -1,37 +1,3 @@
export default function Home() { export default function Home() {
return ( return "helloworld";
<form
method="post"
action="https://listmonk.schulze.network/subscription/form"
className="listmonk-form"
>
<div>
<h3>Subscribe</h3>
<input type="hidden" name="nonce" />
<p>
<input type="email" name="email" required placeholder="E-post" />
</p>
<p>
<input type="text" name="name" placeholder="Namn (ej obligatorisk)" />
</p>
<p>
<input
id="c99ab"
type="checkbox"
name="l"
checked
value="c99ab65c-cd0a-4ccc-ba63-0c9f7de99ac3"
/>
<label htmlFor="c99ab">Produktnyheter</label>
<br />
<span>Produktnyheter för Schulze Network</span>
</p>
<p>
<input type="submit" value="Prenumerera" />
</p>
</div>
</form>
);
} }

17
app/sign-up/page.tsx Normal file
View File

@ -0,0 +1,17 @@
import Image from "next/image";
import SignUp from "./sign-up-form";
export default function Page() {
return (
<div className="w-4/5 max-w-2xl mx-auto my-12">
<Image
src="/image/bam.png"
alt="Bangers and Mash GBG"
width={200}
height={200}
className="mx-auto"
/>
<SignUp />
</div>
);
}

View File

@ -0,0 +1,124 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { signupFormSubmit } from "@/lib/actions";
export const signupFormSchema = z.object({
name: z.string().min(2, { message: "Name is required" }).max(50, { message: "Name is too long" }),
email: z.string().email({ message: "Email is invalid" }),
dob: z.date({ required_error: "Birthday is required" }),
});
export const youngestDate = new Date(new Date().setFullYear(new Date().getFullYear() - 21));
export const oldestDate = new Date(new Date().setFullYear(new Date().getFullYear() - 100));
export default function SignUp() {
const form = useForm<z.infer<typeof signupFormSchema>>({
resolver: zodResolver(signupFormSchema),
defaultValues: {
name: "",
email: "",
dob: youngestDate,
},
});
function onSubmit(values: z.infer<typeof signupFormSchema>) {
signupFormSubmit(values);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="name@example.com" {...field} />
</FormControl>
<FormDescription>
We will contact you here with information about events.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Firstname Lastname" {...field} />
</FormControl>
<FormDescription>Please enter your full name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dob"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Date of birth</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={"outline"}
className={cn(
"w-[240px] pl-3 text-left font-normal",
!field.value && "text-muted-foreground"
)}
>
{field.value ? format(field.value, "PPP") : <span>Pick a date</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
required
mode="single"
showOutsideDays={false}
selected={field.value}
onSelect={field.onChange}
disabled={(date) => date > youngestDate}
defaultMonth={field.value}
fromDate={oldestDate}
toDate={youngestDate}
captionLayout="dropdown"
/>
</PopoverContent>
</Popover>
<FormDescription>You must be over 21 to sign up.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}

View File

@ -0,0 +1,95 @@
"use client";
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex flex-col justify-center pt-1 relative items-stretch space-x-4 w-full",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
Dropdown: ({ children, value, onChange, ...props }) => {
const options = React.Children.toArray(children) as React.ReactElement<
React.HTMLProps<HTMLOptionElement>
>[];
const selected = options.find((child) => child.props.value === value);
const handleChange = (value: string) => {
const changeEvent = {
target: { value },
} as React.ChangeEvent<HTMLSelectElement>;
onChange?.(changeEvent);
};
return (
<Select value={value?.toString()} onValueChange={(v) => handleChange(v)}>
<SelectTrigger className="inline-flex w-2/5 mx-2 mt-2">
<SelectValue>{selected?.props?.children}</SelectValue>
</SelectTrigger>
<SelectContent position="popper">
{options?.map((option, id: number) => (
<SelectItem
key={`${option.props.value}-${id}`}
value={option.props.value?.toString() ?? ""}
>
{option.props.children}
</SelectItem>
))}
</SelectContent>
</Select>
);
},
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";
export { Calendar };

25
components/ui/input.tsx Normal file
View File

@ -0,0 +1,25 @@
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-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

31
components/ui/popover.tsx Normal file
View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

160
components/ui/select.tsx Normal file
View File

@ -0,0 +1,160 @@
"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-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
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 bg-popover text-popover-foreground 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",
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-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-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-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

19
lib/actions.ts Normal file
View File

@ -0,0 +1,19 @@
"use server";
import { z } from "zod";
import { oldestDate, signupFormSchema, youngestDate } from "@/app/sign-up/sign-up-form";
import listmonk from "./listmonk";
export async function signupFormSubmit(data: z.infer<typeof signupFormSchema>) {
if (data.dob > youngestDate || data.dob < oldestDate) {
return { error: "Invalid date of birth" };
}
const listmonkData = {
email: data.email,
name: data.name,
attribs: {
dob: data.dob.toISOString(),
},
};
console.log(listmonkData);
}

21
lib/listmonk.ts Normal file
View File

@ -0,0 +1,21 @@
import "server-only";
async function makeApiCall(url: string, options?: RequestInit) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Error making API call:", error);
throw error;
}
}
async function listmonk() {
const listmonkUrl = process.env.LISTMONK_URL ?? "";
makeApiCall(listmonkUrl);
}
export default listmonk;