mirror of
https://github.com/lukevella/rallly.git
synced 2025-08-06 09:59:00 +02:00
Builder
This commit is contained in:
parent
4e67c783be
commit
c5cf9298cd
14 changed files with 584 additions and 11 deletions
|
@ -64,6 +64,8 @@
|
||||||
"posthog-js": "^1.102.1",
|
"posthog-js": "^1.102.1",
|
||||||
"posthog-node": "^3.6.0",
|
"posthog-node": "^3.6.0",
|
||||||
"react-big-calendar": "^1.8.1",
|
"react-big-calendar": "^1.8.1",
|
||||||
|
"react-day-picker": "^8.10.0",
|
||||||
|
"date-fns": "^3.2.0",
|
||||||
"react-hook-form": "^7.42.1",
|
"react-hook-form": "^7.42.1",
|
||||||
"react-hook-form-persist": "^3.0.0",
|
"react-hook-form-persist": "^3.0.0",
|
||||||
"react-hot-toast": "^2.4.0",
|
"react-hot-toast": "^2.4.0",
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default async function Layout({
|
||||||
<Trans t={t} i18nKey="polls" />
|
<Trans t={t} i18nKey="polls" />
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/new">
|
<Link href="/create">
|
||||||
<PenBoxIcon className="text-muted-foreground size-4" />
|
<PenBoxIcon className="text-muted-foreground size-4" />
|
||||||
<span className="hidden sm:inline">
|
<span className="hidden sm:inline">
|
||||||
<Trans t={t} i18nKey="newPoll" />
|
<Trans t={t} i18nKey="newPoll" />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import { cn } from "@rallly/ui";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
|
@ -19,7 +20,7 @@ export function MenuItem(props: { href: string; children: React.ReactNode }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className={clsx(
|
className={cn(
|
||||||
"flex min-w-0 items-center gap-x-2 rounded-none px-3 py-2 text-sm font-medium",
|
"flex min-w-0 items-center gap-x-2 rounded-none px-3 py-2 text-sm font-medium",
|
||||||
pathname === props.href
|
pathname === props.href
|
||||||
? "bg-white"
|
? "bg-white"
|
||||||
|
|
453
apps/web/src/app/[locale]/create/create-form.tsx
Normal file
453
apps/web/src/app/[locale]/create/create-form.tsx
Normal file
|
@ -0,0 +1,453 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { IfNeedBeIcon, NoIcon, YesIcon } from "@rallly/icons";
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import { Calendar } from "@rallly/ui/calendar";
|
||||||
|
import { Checkbox } from "@rallly/ui/checkbox";
|
||||||
|
import { DropdownMenu, DropdownMenuTrigger } from "@rallly/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from "@rallly/ui/form";
|
||||||
|
import { Input } from "@rallly/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@rallly/ui/select";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
|
||||||
|
import { Textarea } from "@rallly/ui/textarea";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import {
|
||||||
|
ArrowLeftIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
ListIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import React from "react";
|
||||||
|
import { useForm, useFormContext } from "react-hook-form";
|
||||||
|
import useFormPersist from "react-hook-form-persist";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import {
|
||||||
|
InviteCard,
|
||||||
|
InviteCardForm,
|
||||||
|
InviteCardGeneral,
|
||||||
|
} from "@/app/[locale]/create/invite-card";
|
||||||
|
import {
|
||||||
|
PageContainer,
|
||||||
|
PageContent,
|
||||||
|
PageHeader,
|
||||||
|
} from "@/app/components/page-layout";
|
||||||
|
import { useQueryString } from "@/utils/query-string";
|
||||||
|
|
||||||
|
export function FieldGroup({ children }: { children: React.ReactNode }) {
|
||||||
|
return <div className="space-y-4">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldGroupTitle({ children }: { children: React.ReactNode }) {
|
||||||
|
return <h2 className="font-semibold">{children}</h2>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldGroupContent({ children }: { children: React.ReactNode }) {
|
||||||
|
return <div className="space-y-6">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarTitle({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<h3 className="sticky top-14 flex items-center gap-x-2 border-b bg-gray-50 px-5 py-4 text-sm font-bold">
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarGroup({ children }: { children: React.ReactNode }) {
|
||||||
|
return <div className="space-y-4">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarSection({ children }: { children?: React.ReactNode }) {
|
||||||
|
return <section>{children}</section>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarContent({ children }: { children?: React.ReactNode }) {
|
||||||
|
return <div className="px-5 py-4">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function GeneralForm() {
|
||||||
|
const form = useFormContext<FormData>();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<SidebarSection>
|
||||||
|
<SidebarContent>
|
||||||
|
<FieldGroupContent>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="event.title"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Title</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder={t("titlePlaceholder")} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="event.description"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("description")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
placeholder={t("descriptionPlaceholder")}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="event.location"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("location")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder={t("locationPlaceholder")} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FieldGroupContent>
|
||||||
|
</SidebarContent>
|
||||||
|
</SidebarSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormData = {
|
||||||
|
event: {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
location?: string;
|
||||||
|
timezone?: string; // undefined if meeting is remote
|
||||||
|
};
|
||||||
|
form: {
|
||||||
|
type: "poll";
|
||||||
|
poll: {
|
||||||
|
prompt: string;
|
||||||
|
duration: number;
|
||||||
|
options: {
|
||||||
|
start: Date;
|
||||||
|
}[];
|
||||||
|
settings: {
|
||||||
|
hideParticipants: boolean;
|
||||||
|
hideScores: boolean;
|
||||||
|
allowTentative: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
advanced: {
|
||||||
|
requireParticipantEmail: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function PollForm() {
|
||||||
|
const form = useFormContext<FormData>();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<SidebarSection>
|
||||||
|
<SidebarContent>
|
||||||
|
<FieldGroup>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="form.poll.prompt"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("prompt", {
|
||||||
|
defaultValue: "Prompt",
|
||||||
|
})}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder={t("pollPromptPlaceholder", {
|
||||||
|
defaultValue: "Select as many dates as you can attend",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="form.poll.options"
|
||||||
|
defaultValue={[]}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("options", {
|
||||||
|
defaultValue: "Options",
|
||||||
|
})}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Calendar
|
||||||
|
selected={field.value.map((option) => option.start)}
|
||||||
|
onSelectedChange={(selection) => {
|
||||||
|
field.onChange(
|
||||||
|
selection?.map(
|
||||||
|
(date) =>
|
||||||
|
({
|
||||||
|
start: date,
|
||||||
|
}) ?? [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="form.poll.duration"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("duration", {
|
||||||
|
defaultValue: "Duration",
|
||||||
|
})}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={field.value?.toString()}
|
||||||
|
onValueChange={(newValue) => {
|
||||||
|
field.onChange(parseInt(newValue));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger ref={field.ref}>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="0">All-day</SelectItem>
|
||||||
|
<SelectItem value="15">15 minutes</SelectItem>
|
||||||
|
<SelectItem value="30">30 minutes</SelectItem>
|
||||||
|
<SelectItem value="45">45 minutes</SelectItem>
|
||||||
|
<SelectItem value="60">1 hour</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{form.watch("form.poll.duration") === 0 ? (
|
||||||
|
<div>all-day</div>
|
||||||
|
) : (
|
||||||
|
<div>time</div>
|
||||||
|
)}
|
||||||
|
</FieldGroup>
|
||||||
|
</SidebarContent>
|
||||||
|
</SidebarSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Poll({
|
||||||
|
prompt,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
prompt: string;
|
||||||
|
options: { start: Date }[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
<h3 className="sticky top-0 -mx-6 -mt-6 p-6 font-semibold">{prompt}</h3>
|
||||||
|
<ul className="mb-4 flex gap-x-4">
|
||||||
|
<li className="flex items-center gap-x-2">
|
||||||
|
<YesIcon className="size-5 text-green-500" />
|
||||||
|
<span className="text-sm">Yes</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-x-2">
|
||||||
|
<IfNeedBeIcon className="size-5 text-amber-400" />
|
||||||
|
<span className="text-sm">If need be</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-x-2">
|
||||||
|
<NoIcon className="size-5 text-gray-400" />
|
||||||
|
<span className="text-sm">No</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div className="flex grow gap-x-4">
|
||||||
|
<div className="grow">
|
||||||
|
<div className="grid grow gap-2">
|
||||||
|
{options.map((option, i) => (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-x-4 rounded-md border bg-white px-4 py-3"
|
||||||
|
key={i}
|
||||||
|
>
|
||||||
|
<Checkbox id={`option${i}`} name={`option${i}`} />
|
||||||
|
<label className="grow font-medium" htmlFor={`option${i}`}>
|
||||||
|
{dayjs(option.start).format("DD MMM YYYY")}
|
||||||
|
</label>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
<MoreHorizontalIcon className="text-muted-foreground size-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <div>
|
||||||
|
<Calendar
|
||||||
|
disabled={(date) => {
|
||||||
|
return !options.some((option) =>
|
||||||
|
dayjs(option.start).isSame(date, "day"),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateFormInput() {
|
||||||
|
const form = useFormContext<FormData>();
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const pathname = usePathname();
|
||||||
|
return (
|
||||||
|
<SidebarGroup>
|
||||||
|
<Tabs
|
||||||
|
onValueChange={(newValue) => {
|
||||||
|
const queryString = new URLSearchParams(searchParams?.toString());
|
||||||
|
queryString.set("tab", newValue);
|
||||||
|
router.replace(pathname + `?${queryString.toString()}`);
|
||||||
|
}}
|
||||||
|
value={searchParams?.get("tab") ?? "event"}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
<TabsList className="w-full">
|
||||||
|
<TabsTrigger
|
||||||
|
value="event"
|
||||||
|
className="flex grow items-center gap-x-2"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="size-4" /> Event
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="form"
|
||||||
|
className="flex grow items-center gap-x-2"
|
||||||
|
>
|
||||||
|
<ListIcon className="size-4" />
|
||||||
|
Form
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
<TabsContent value="event">
|
||||||
|
<GeneralForm />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="form">
|
||||||
|
<PollForm />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</SidebarGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateFormPreview() {
|
||||||
|
const form = useFormContext<FormData>();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InviteCard className="mx-auto">
|
||||||
|
<InviteCardGeneral
|
||||||
|
title={form.watch("event.title") || t("titlePlaceholder")}
|
||||||
|
location={form.watch("event.location") || t("locationPlaceholder")}
|
||||||
|
description={
|
||||||
|
form.watch("event.description") || t("descriptionPlaceholder")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InviteCardForm>
|
||||||
|
<div
|
||||||
|
className="h-full rounded-md"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-muted-foreground">Step 1 of 2</div>
|
||||||
|
{form.watch("form.type") === "poll" && (
|
||||||
|
<Poll
|
||||||
|
prompt={
|
||||||
|
form.watch("form.poll.prompt") ||
|
||||||
|
t("pollPromptPlaceholder", {
|
||||||
|
defaultValue: "Select as many dates as you can attend",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
options={form.watch("form.poll.options", [])}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</InviteCardForm>
|
||||||
|
</InviteCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateForm() {
|
||||||
|
const form = useForm<FormData>();
|
||||||
|
|
||||||
|
const { clear } = useFormPersist("create-form", {
|
||||||
|
watch: form.watch,
|
||||||
|
setValue: form.setValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form className="flex h-screen w-full flex-col" onSubmit={() => {}}>
|
||||||
|
<PageHeader className="flex justify-between">
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
onClick={() => {
|
||||||
|
clear();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link href="/polls">
|
||||||
|
<ArrowLeftIcon className="text-muted-foreground size-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary">Create</Button>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div className="flex grow">
|
||||||
|
<div className="shadow-huge m-4 w-96 rounded-md border bg-gray-50">
|
||||||
|
<CreateFormInput />
|
||||||
|
</div>
|
||||||
|
<PageContainer className="sticky top-0 hidden grow items-center justify-center lg:flex">
|
||||||
|
<PageContent>
|
||||||
|
<CreateFormPreview />
|
||||||
|
</PageContent>
|
||||||
|
</PageContainer>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
69
apps/web/src/app/[locale]/create/invite-card.tsx
Normal file
69
apps/web/src/app/[locale]/create/invite-card.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import { cn } from "@rallly/ui";
|
||||||
|
import { GlobeIcon, MapPinIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { LogoLink } from "@/app/components/logo-link";
|
||||||
|
|
||||||
|
export function InviteCardGeneral({
|
||||||
|
title,
|
||||||
|
location,
|
||||||
|
description,
|
||||||
|
}: {
|
||||||
|
title?: React.ReactNode;
|
||||||
|
location?: React.ReactNode;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<aside className="border-primary w-72 shrink-0 p-6">
|
||||||
|
<LogoLink />
|
||||||
|
<h1 className="my-4 text-xl font-bold">{title}</h1>
|
||||||
|
{description ? (
|
||||||
|
<p className="text-muted-foreground leading-relaxed">{description}</p>
|
||||||
|
) : null}
|
||||||
|
<ul className="mt-6 space-y-2.5">
|
||||||
|
{location ? (
|
||||||
|
<li className="text-muted-foreground flex items-center text-sm leading-relaxed">
|
||||||
|
<MapPinIcon className="mr-2 inline-block h-4 w-4" />
|
||||||
|
<span>{location}</span>
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
<li className="text-muted-foreground flex items-center text-sm leading-relaxed">
|
||||||
|
<GlobeIcon className="mr-2 inline-block h-4 w-4" />
|
||||||
|
<span>Europe/London</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InviteCardForm({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={cn("h-[400px] min-h-0 w-96 grow overflow-auto", className)}>
|
||||||
|
<div className="p-6">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InviteCard({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"shadow-huge flex max-h-full min-h-0 max-w-4xl rounded-lg bg-white",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
3
apps/web/src/app/[locale]/create/loading.tsx
Normal file
3
apps/web/src/app/[locale]/create/loading.tsx
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default function Loading() {
|
||||||
|
return null;
|
||||||
|
}
|
17
apps/web/src/app/[locale]/create/page.tsx
Normal file
17
apps/web/src/app/[locale]/create/page.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { CreateForm } from "@/app/[locale]/create/create-form";
|
||||||
|
import { getTranslation } from "@/app/i18n";
|
||||||
|
|
||||||
|
export default async function Page({ params }: { params: { locale: string } }) {
|
||||||
|
return <CreateForm />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: { locale: string };
|
||||||
|
}) {
|
||||||
|
const { t } = await getTranslation(params.locale);
|
||||||
|
return {
|
||||||
|
title: t("newPoll"),
|
||||||
|
};
|
||||||
|
}
|
19
apps/web/src/utils/query-string.ts
Normal file
19
apps/web/src/utils/query-string.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { usePathname, useSearchParams } from "next/navigation";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function useQueryString() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const createQueryString = React.useCallback(
|
||||||
|
(name: string, value: string) => {
|
||||||
|
const params = new URLSearchParams(searchParams?.toString());
|
||||||
|
params.set(name, value);
|
||||||
|
|
||||||
|
return pathname + "?" + params.toString();
|
||||||
|
},
|
||||||
|
[searchParams, pathname],
|
||||||
|
);
|
||||||
|
return { queryString: searchParams, createQueryString };
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@ui/utils";
|
import { cn } from "@rallly/ui";
|
||||||
import { Button } from "@ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
@ -181,7 +181,7 @@ const Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group relative flex h-full w-full items-start justify-end rounded-none px-2.5 py-1.5 text-xs font-medium tracking-tight focus:z-10 focus:rounded",
|
"group relative flex h-full w-full items-start justify-end rounded-none px-2.5 py-1.5 text-sm font-medium tracking-tight focus:z-10 focus:rounded",
|
||||||
{
|
{
|
||||||
"text-rose-600": day.today && !day.selected,
|
"text-rose-600": day.today && !day.selected,
|
||||||
"bg-gray-50 text-gray-500": day.outOfMonth && !day.isPast,
|
"bg-gray-50 text-gray-500": day.outOfMonth && !day.isPast,
|
||||||
|
|
|
@ -93,7 +93,7 @@ const FormLabel = React.forwardRef<
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(error && "text-destructive", className)}
|
className={cn("font-medium", error && "text-destructive", className)}
|
||||||
htmlFor={formItemId}
|
htmlFor={formItemId}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export { cn } from "@rallly/ui";
|
export { cn } from "./lib/utils";
|
||||||
|
|
|
@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full items-center justify-between rounded-md border bg-white px-3 py-2 text-sm focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-10 w-full items-center justify-between rounded-md border bg-white px-3 py-2 text-base focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -80,7 +80,7 @@ const SelectItem = React.forwardRef<
|
||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-2 pl-8 pr-2 text-sm outline-none focus:bg-gray-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-2 pl-8 pr-2 text-base outline-none focus:bg-gray-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { cn } from "@rallly/ui";
|
import { cn } from "@rallly/ui";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root;
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -5791,6 +5791,11 @@ date-arithmetic@^4.1.0:
|
||||||
resolved "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz"
|
resolved "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz"
|
||||||
integrity sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==
|
integrity sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==
|
||||||
|
|
||||||
|
date-fns@^3.2.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.2.0.tgz#c97cf685b62c829aa4ecba554e4a51768cf0bffc"
|
||||||
|
integrity sha512-E4KWKavANzeuusPi0jUjpuI22SURAznGkx7eZV+4i6x2A+IZxAMcajgkvuDAU1bg40+xuhW1zRdVIIM/4khuIg==
|
||||||
|
|
||||||
dayjs@^1.11.10:
|
dayjs@^1.11.10:
|
||||||
version "1.11.10"
|
version "1.11.10"
|
||||||
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz"
|
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz"
|
||||||
|
@ -10071,6 +10076,11 @@ react-big-calendar@^1.8.1:
|
||||||
react-overlays "^5.2.1"
|
react-overlays "^5.2.1"
|
||||||
uncontrollable "^7.2.1"
|
uncontrollable "^7.2.1"
|
||||||
|
|
||||||
|
react-day-picker@^8.10.0:
|
||||||
|
version "8.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.10.0.tgz#729c5b9564967a924213978fb9c0751884a60595"
|
||||||
|
integrity sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==
|
||||||
|
|
||||||
react-dom@18.2.0, react-dom@^18.2.0:
|
react-dom@18.2.0, react-dom@^18.2.0:
|
||||||
version "18.2.0"
|
version "18.2.0"
|
||||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
|
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue