);
}
diff --git a/apps/web/src/app/(site)/page.tsx b/apps/web/src/app/(site)/page.tsx
index 28f4d68..cbbcafc 100644
--- a/apps/web/src/app/(site)/page.tsx
+++ b/apps/web/src/app/(site)/page.tsx
@@ -1,9 +1,5 @@
"use client";
-export default function Web() {
- return (
-
- );
-}
+import { Landing } from "components/Landing";
+
+export default Landing;
diff --git a/apps/web/src/app/(site)/pricing/page.tsx b/apps/web/src/app/(site)/pricing/page.tsx
index 0d483c6..3e029c7 100644
--- a/apps/web/src/app/(site)/pricing/page.tsx
+++ b/apps/web/src/app/(site)/pricing/page.tsx
@@ -1,8 +1,8 @@
"use client";
import { ActivePlans, Plan } from "@letsgo/pricing";
-import { PlanSelector } from "../../../components/PlanSelector";
-import { useTenant } from "../../../components/TenantProvider";
+import { PlanSelector } from "components/PlanSelector";
+import { useTenant } from "components/TenantProvider";
import { useRouter } from "next/navigation";
export default function Pricing() {
@@ -32,13 +32,17 @@ export default function Pricing() {
};
return (
-
-
Pricing
-
+
);
}
diff --git a/apps/web/src/app/global.css b/apps/web/src/app/global.css
new file mode 100644
index 0000000..6a75725
--- /dev/null
+++ b/apps/web/src/app/global.css
@@ -0,0 +1,76 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
\ No newline at end of file
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 1bf96e4..20a8c3b 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -1,5 +1,14 @@
import { UserProvider } from "@auth0/nextjs-auth0/client";
-import { TenantProvider } from "../components/TenantProvider";
+import { TenantProvider } from "components/TenantProvider";
+import { Inter as FontSans } from "next/font/google";
+import { Toaster } from "components/ui/toaster";
+import { cn } from "components/utils";
+import "app/global.css";
+
+export const fontSans = FontSans({
+ subsets: ["latin"],
+ variable: "--font-sans",
+});
export default function RootLayout({
children,
@@ -7,12 +16,18 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
-
-
-
- {children}
-
-
+
+
+
+ {children}
+
+
+
);
}
diff --git a/apps/web/src/components/Account.tsx b/apps/web/src/components/Account.tsx
index 74e95d9..6c6722a 100644
--- a/apps/web/src/components/Account.tsx
+++ b/apps/web/src/components/Account.tsx
@@ -1,8 +1,10 @@
"use client";
import { getPlan } from "@letsgo/pricing";
-import { useRouter } from "next/navigation";
-import { useState } from "react";
+import { Billing } from "./Billing";
+import { DeleteTenant } from "./DeleteTenant";
+import { LoadingPlaceholder } from "./LoadingPlaceholder";
+import { SubscriptionPlan } from "./SuscriptionPlan";
import { useTenant } from "./TenantProvider";
import { useApiMutate } from "./common-client";
@@ -11,9 +13,7 @@ export interface AccountProps {
}
export function Account({ tenantId }: AccountProps) {
- const router = useRouter();
- const { isLoading, error: tenantError, currentTenant } = useTenant();
- const [confirmDeleteTenant, setConfirmDeleteTenant] = useState(false);
+ const { error: tenantError, currentTenant } = useTenant();
const {
isMutating: isDeletingTenant,
error: errorDeletingTenant,
@@ -26,82 +26,27 @@ export function Account({ tenantId }: AccountProps) {
},
});
- if (isLoading) return
Loading...
;
- if (isDeletingTenant) return
Deleting...
;
const error = tenantError || errorDeletingTenant;
if (error) throw error;
const plan = getPlan(currentTenant?.plan.planId || "");
const isStripePlan = plan?.usesStripe;
- const handleChangePlan = async () => {
- router.push(`/manage/${tenantId}/newplan`);
- };
-
- const handleChangePaymentMethod = async () => {
- router.push(`/manage/${tenantId}/paymentmethod`);
- };
-
const handleDelete = async () => {
- if (!confirmDeleteTenant) {
- setConfirmDeleteTenant(true);
- } else {
- deleteTenant();
- }
+ deleteTenant();
};
- return (
-
-
- Your current subscription plan is:{" "}
- {plan ? `${plan.name} (${plan.price})` : "Unknown"}
-
- {isStripePlan && currentTenant?.plan.stripeSubscription && (
-
- Billing status: {currentTenant.plan.stripeSubscription.status} {" "}
- {currentTenant.plan.stripeSubscription.card && (
-
- ({currentTenant.plan.stripeSubscription.card.brand} ••••{" "}
- {currentTenant.plan.stripeSubscription.card.last4})
-
- )}
-
- )}
- {isStripePlan &&
- currentTenant?.plan.stripeSubscription?.currentPeriodEnd && (
-
- Current period ends:{" "}
-
- {new Date(
- currentTenant?.plan.stripeSubscription?.currentPeriodEnd
- ).toDateString()}
-
-
- )}
- {!confirmDeleteTenant && (
-
- Change plan {" "}
- {plan?.usesStripe && (
-
-
- Change payment method
- {" "}
-
- )}
-
- )}
- {confirmDeleteTenant && (
-
- Do you really want to delete the tenant and all its data? It cannot be
- undone.
-
- )}
-
- {confirmDeleteTenant ? "Yes" : "Delete tenant"}
- {" "}
- {confirmDeleteTenant && (
-
setConfirmDeleteTenant(false)}>No
- )}
+ return !currentTenant || isDeletingTenant ? (
+
+ ) : (
+
);
}
diff --git a/apps/web/src/components/Billing.tsx b/apps/web/src/components/Billing.tsx
new file mode 100644
index 0000000..a85610c
--- /dev/null
+++ b/apps/web/src/components/Billing.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import { Tenant } from "@letsgo/tenant";
+import { Button } from "components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "components/ui/card";
+import { useRouter } from "next/navigation";
+
+export interface BillingProps {
+ tenant: Tenant;
+}
+
+export function Billing({ tenant }: BillingProps) {
+ const router = useRouter();
+
+ const subscription = tenant.plan.stripeSubscription;
+
+ const handleChangePaymentMethod = async () => {
+ router.push(`/manage/${tenant.tenantId}/paymentmethod`);
+ };
+
+ return (
+
+
+ Billing
+
+ {subscription && (
+
+
+
Status
+
+ {subscription.status}
+
+
+ {subscription.card && (
+
+
Payment method
+
+ {subscription.card.brand} •••• {subscription.card.last4}
+
+
+ )}
+
+
+ Current period ends
+
+
+ {new Date(subscription.currentPeriodEnd).toDateString()}
+
+
+
+
+
+ Change payment method
+
+
+
+
+ )}
+ {!subscription && (
+
+ No billing information on file.
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/components/Checkout.tsx b/apps/web/src/components/Checkout.tsx
index 3852ad3..192c3af 100644
--- a/apps/web/src/components/Checkout.tsx
+++ b/apps/web/src/components/Checkout.tsx
@@ -5,9 +5,12 @@ import {
useElements,
useStripe,
} from "@stripe/react-stripe-js";
+import { Button } from "components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "components/ui/card";
import { useRouter, useSearchParams } from "next/navigation";
import { FormEventHandler, MouseEventHandler, useState } from "react";
import { createAbsoluteUrl } from "./common-client";
+import { Badge } from "./ui/badge";
interface CheckoutProps {
tenantId: string;
@@ -37,7 +40,7 @@ function Checkout({ tenantId, title, mode }: CheckoutProps) {
? await stripe.confirmPayment({
elements,
confirmParams: {
- return_url: createAbsoluteUrl(`/manage/${tenantId}/settings`),
+ return_url: createAbsoluteUrl(`/manage/${tenantId}/subscription`),
},
})
: await stripe.confirmSetup({
@@ -56,24 +59,40 @@ function Checkout({ tenantId, title, mode }: CheckoutProps) {
const handleCancel: MouseEventHandler
= async (event) => {
event.preventDefault();
- router.replace(`/manage/${tenantId}/settings`);
+ router.replace(`/manage/${tenantId}/subscription`);
};
return (
-
-
{title}
- {message &&
{message}
}
-
-
+
+
+ {title}
+
+
+ {message && (
+
+ {message}
+
+ )}
+
+
+
);
}
+
export default Checkout;
diff --git a/apps/web/src/components/ConfirmDialog.tsx b/apps/web/src/components/ConfirmDialog.tsx
new file mode 100644
index 0000000..2591191
--- /dev/null
+++ b/apps/web/src/components/ConfirmDialog.tsx
@@ -0,0 +1,49 @@
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "components/ui/dialog";
+import { Button } from "./ui/button";
+
+export interface ConfirmDialogProps {
+ title: string;
+ description: React.ReactNode;
+ trigger: React.ReactNode;
+ confirm: React.ReactNode;
+ cancel?: React.ReactNode;
+ onConfirm: () => Promise;
+}
+
+export function ConfirmDialog({
+ title,
+ description,
+ trigger,
+ confirm,
+ cancel,
+ onConfirm,
+}: ConfirmDialogProps) {
+ return (
+
+ {trigger}
+
+
+ {title}
+ {description}
+
+
+
+ {cancel || Cancel }
+
+
+ {confirm}
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/Contact.tsx b/apps/web/src/components/Contact.tsx
new file mode 100644
index 0000000..9441a14
--- /dev/null
+++ b/apps/web/src/components/Contact.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import { useUser } from "@auth0/nextjs-auth0/client";
+import { useEffect, useState } from "react";
+import { useSearchParams } from "next/navigation";
+import { useTenant } from "components/TenantProvider";
+import { useApiMutate } from "components/common-client";
+import { ContactMessagePayload } from "@letsgo/types";
+import { Card, CardContent, CardHeader, CardTitle } from "components/ui/card";
+import { Label } from "components/ui/label";
+import { Input } from "components/ui/input";
+import { Textarea } from "components/ui/textarea";
+import { Button } from "components/ui/button";
+import { useToast } from "components/ui/use-toast";
+
+interface ContactParams {
+ email: string;
+ name: string;
+ message: string;
+}
+
+export function Contact() {
+ const query = useSearchParams();
+ const { toast } = useToast();
+ const { isLoading: isUserLoading, user } = useUser();
+ const { currentTenant } = useTenant();
+ const [params, setParams] = useState({
+ email: "",
+ name: "",
+ message: "",
+ });
+ const {
+ isMutating: isSubmitting,
+ error: errorSubmiting,
+ trigger: submitContact,
+ } = useApiMutate({
+ path: `/v1/contact`,
+ method: "POST",
+ unauthenticated: true,
+ afterSuccess: async () => {
+ setParams({ email: "", name: "", message: "" });
+ toast({
+ title: "Message sent - thank you!",
+ });
+ },
+ });
+
+ useEffect(() => {
+ if (user && (!params.email || !params.name)) {
+ setParams({
+ email: params.email || user.email || "",
+ name: params.name || user.name || "",
+ message: params.message,
+ });
+ }
+ }, [user, params.email, params.name, params.message]);
+
+ if (errorSubmiting) throw errorSubmiting;
+
+ const handleSubmit = () => {
+ const payload: ContactMessagePayload = {
+ ...params,
+ query: Object.fromEntries(query.entries()),
+ tenantId: currentTenant?.tenantId,
+ identityId: user?.identityId as string,
+ timestamp: new Date().toISOString(),
+ };
+ submitContact(payload);
+ };
+
+ return (
+
+
+
+ Contact Us
+
+
+
+ Name
+ setParams({ ...params, name: e.target.value })}
+ />
+
+
+ Email
+ setParams({ ...params, email: e.target.value })}
+ />
+
+
+ Message
+
+
+
+ Submit
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/CopyButton.tsx b/apps/web/src/components/CopyButton.tsx
new file mode 100644
index 0000000..1525038
--- /dev/null
+++ b/apps/web/src/components/CopyButton.tsx
@@ -0,0 +1,59 @@
+"use client";
+
+import { Copy } from "lucide-react";
+import { Button } from "components/ui/button";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "components/ui/tooltip";
+import { useEffect, useState } from "react";
+
+export interface CopyButtonProps {
+ text: string;
+ tooltipText?: string;
+ buttonClassName?: string;
+ iconClassName?: string;
+}
+
+export function CopyButton({
+ text,
+ tooltipText = "Copy",
+ buttonClassName = "h-8 p-2.5",
+ iconClassName = "w-3 h-3",
+}: CopyButtonProps) {
+ const [enableToolTip, setEnableToolTip] = useState(false);
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(text);
+ };
+
+ // Workaround for the tooltip showing up by default on first render in a popup.
+ useEffect(() => {
+ if (!enableToolTip) {
+ setTimeout(() => {
+ setEnableToolTip(true);
+ }, 100);
+ }
+ }, [enableToolTip, setEnableToolTip]);
+
+ const button = (
+
+
+
+ );
+
+ return enableToolTip ? (
+
+
+ {button}
+
+ {tooltipText}
+
+
+
+ ) : (
+ button
+ );
+}
diff --git a/apps/web/src/components/Dashboard.tsx b/apps/web/src/components/Dashboard.tsx
new file mode 100644
index 0000000..80da717
--- /dev/null
+++ b/apps/web/src/components/Dashboard.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+import { DraftingCompass } from "lucide-react";
+
+export interface DashboardProps {
+ tenantId: string;
+}
+
+export function Dashboard({ tenantId }: DashboardProps) {
+ return (
+
+
+
Future home of your product's dashboard - make it yours
+
+ );
+}
diff --git a/apps/web/src/components/DeleteTenant.tsx b/apps/web/src/components/DeleteTenant.tsx
new file mode 100644
index 0000000..84002d2
--- /dev/null
+++ b/apps/web/src/components/DeleteTenant.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { Tenant } from "@letsgo/tenant";
+import { Button } from "components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "components/ui/card";
+import { useRouter } from "next/navigation";
+import { ConfirmDialog } from "./ConfirmDialog";
+
+export interface DeleteTenantProps {
+ tenant: Tenant;
+ onDeleteTenant: () => Promise;
+}
+
+export function DeleteTenant({ tenant, onDeleteTenant }: DeleteTenantProps) {
+ const router = useRouter();
+
+ return (
+
+
+ Danger zone
+
+
+
+ Delete tenant}
+ confirm={Delete }
+ onConfirm={onDeleteTenant}
+ />
+
+
+
+ );
+}
diff --git a/apps/web/src/components/Invitations.tsx b/apps/web/src/components/Invitations.tsx
index 09ba366..90feded 100644
--- a/apps/web/src/components/Invitations.tsx
+++ b/apps/web/src/components/Invitations.tsx
@@ -2,20 +2,32 @@
import { Invitation } from "@letsgo/tenant";
import { GetInvitationsResponse } from "@letsgo/types";
-import { CSSProperties, useEffect, useState } from "react";
+import { useEffect, useState } from "react";
import { useApi, useApiMutate } from "./common-client";
-const style: CSSProperties = {
- border: "1px solid black",
- borderCollapse: "collapse",
- padding: "0.5em",
-};
+import { Button } from "components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "components/ui/card";
+import { Trash2 } from "lucide-react";
+import { CopyButton } from "./CopyButton";
+import { LoadingPlaceholder } from "./LoadingPlaceholder";
+import { useToast } from "components/ui/use-toast";
+
+function getInvitationUrl(tenantId: string, invitationId: string) {
+ return `${window.location.origin}/join/${tenantId}/${invitationId}`;
+}
export interface InvitationsProps {
tenantId: string;
}
export function Invitations({ tenantId }: InvitationsProps) {
+ const { toast } = useToast();
const {
isLoading: isLoadingInvitations,
error: errorLoadingInitations,
@@ -32,6 +44,16 @@ export function Invitations({ tenantId }: InvitationsProps) {
} = useApiMutate({
path: `/v1/tenant/${tenantId}/invitation`,
method: "POST",
+ afterSuccess: async (result) => {
+ const invitationUrl = getInvitationUrl(
+ tenantId,
+ (result as Invitation).invitationId
+ );
+ navigator.clipboard.writeText(invitationUrl);
+ toast({
+ title: "New invitation URL copied to clipboard",
+ });
+ },
});
const [deleteInvitationId, setDeleteInvitationId] = useState(
null
@@ -53,7 +75,6 @@ export function Invitations({ tenantId }: InvitationsProps) {
}
}, [deleteInvitation, deleteInvitationId]);
- if (isLoadingInvitations) return Loading...
;
const error =
errorLoadingInitations ||
errorCreatingInvitation ||
@@ -68,55 +89,81 @@ export function Invitations({ tenantId }: InvitationsProps) {
createInvitation();
};
- const invitationsComponent = !invitations?.length ? (
- No active invitations.
- ) : (
-
-
-
-
- Url
- Created
- Expires
- Action
-
-
-
- {(invitations || []).map((invitation) => (
-
-
- {window.location.origin}/join/{tenantId}/
- {invitation.invitationId}
-
- {new Date(invitation.createdAt).toString()}
- {new Date(invitation.expiresAt).toString()}
-
-
- Revoke
-
-
-
- ))}
-
-
-
- NOTE: Invitation URLs are tenant-specific, confidential, can be used by
- anyone, and expire after 24h. Send them to the intended recipient using
- a trusted channel, e.g. e-mail.
-
-
- );
+ const getTimeRemaining = (invitation: Invitation) => {
+ const expiresAt = new Date(invitation.expiresAt).getTime();
+ const now = new Date().getTime();
+ const remaining = expiresAt - now;
+ const hours = Math.floor(remaining / 1000 / 60 / 60);
+ const minutes = Math.floor(
+ (remaining - hours * 1000 * 60 * 60) / 1000 / 60
+ );
+ return `${hours}h${String(minutes).padStart(2, "0")}m`;
+ };
- return (
-
- {invitationsComponent}
-
-
- {isCreatingInvitation ? "Creating invitation..." : "Create invitation"}
-
-
+ return isLoadingInvitations ? (
+
+
+ Invitations
+
+
+
+
+
+ ) : (
+
+
+ Invitations
+ {!invitations?.length ? (
+
+ To add new members to the team, create an invitation.
+
+ ) : (
+
+ Invitations to join the team are confidential, can be used by
+ anyone, and expire after 24h. Send the invitation URL to the
+ intended recipient using a trusted channel, e.g. e-mail.
+
+ )}
+
+
+ {(invitations || []).map((invitation) => (
+
+
+
+
+ {invitation.invitationId}
+
+
+ Expires in: {getTimeRemaining(invitation)}
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ Create invitation
+
+
+
+
);
}
diff --git a/apps/web/src/components/Landing.tsx b/apps/web/src/components/Landing.tsx
new file mode 100644
index 0000000..aa796aa
--- /dev/null
+++ b/apps/web/src/components/Landing.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+import { DraftingCompass } from "lucide-react";
+
+export interface LandingProps {}
+
+export function Landing({}: LandingProps) {
+ return (
+
+
+
+
Future landing page of your product - make it yours
+
+
+ );
+}
diff --git a/apps/web/src/components/LoadingPlaceholder.tsx b/apps/web/src/components/LoadingPlaceholder.tsx
new file mode 100644
index 0000000..b1c2e65
--- /dev/null
+++ b/apps/web/src/components/LoadingPlaceholder.tsx
@@ -0,0 +1,13 @@
+import { Skeleton } from "components/ui/skeleton";
+
+export function LoadingPlaceholder() {
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/Me.tsx b/apps/web/src/components/Me.tsx
deleted file mode 100644
index ef9c49f..0000000
--- a/apps/web/src/components/Me.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-"use client";
-
-import { GetMeResponse } from "@letsgo/types";
-import { useApi } from "./common-client";
-
-export function Me() {
- /**
- * Use the /api/proxy proxy route of the Next.js web app to proxy the request to /v1/me API of the API server.
- * The proxy route will add the authorization header with the access token of the currently logged in user
- * on the server side of the Next.js app, so there is no need to specify the access token here.
- */
- const { isLoading, error, data } = useApi({
- path: `/v1/me`,
- });
- if (isLoading) return Loading...
;
- if (error) throw error;
-
- return (
-
-
{JSON.stringify(data, null, 2)}
-
- );
-}
diff --git a/apps/web/src/components/Navbar.tsx b/apps/web/src/components/Navbar.tsx
index 22064d0..3ac6d34 100644
--- a/apps/web/src/components/Navbar.tsx
+++ b/apps/web/src/components/Navbar.tsx
@@ -1,9 +1,9 @@
export default function Navbar({ children }: { children: React.ReactNode }) {
- const style = {
- display: "flex",
- width: "100%",
- justifyContent: "space-between",
- };
-
- return {children}
;
+ return (
+
+ );
}
diff --git a/apps/web/src/components/PlanOption.tsx b/apps/web/src/components/PlanOption.tsx
new file mode 100644
index 0000000..b9a326e
--- /dev/null
+++ b/apps/web/src/components/PlanOption.tsx
@@ -0,0 +1,63 @@
+"use client";
+
+import { Plan } from "@letsgo/pricing";
+import { Button } from "components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "components/ui/card";
+
+interface PlanOptionProps {
+ plan: Plan;
+ isCurrentPlan: boolean;
+ actionVerb?: string;
+ onPlanSelected?: () => Promise;
+}
+
+export function PlanOption({
+ plan,
+ isCurrentPlan,
+ actionVerb,
+ onPlanSelected,
+}: PlanOptionProps) {
+ return (
+
+
+ {plan.name}
+
+ {plan.descripton}
+
+
+
+
+ {plan.price || }
+
+
+
+ {plan.features.map((feature) => (
+ {feature}
+ ))}
+
+
+ {onPlanSelected ? (
+
+ {isCurrentPlan ? (
+
+ Your current plan
+
+ ) : (
+ actionVerb && (
+ {actionVerb}
+ )
+ )}
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/src/components/PlanSelector.tsx b/apps/web/src/components/PlanSelector.tsx
index 01d258d..7cf8a96 100644
--- a/apps/web/src/components/PlanSelector.tsx
+++ b/apps/web/src/components/PlanSelector.tsx
@@ -1,58 +1,7 @@
"use client";
import { Plan } from "@letsgo/pricing";
-
-interface PlanOptionProps {
- plan: Plan;
- isCurrentPlan: boolean;
- actionVerb: string;
- onPlanSelected: () => Promise;
-}
-
-function PlanOption({
- plan,
- isCurrentPlan,
- actionVerb,
- onPlanSelected,
-}: PlanOptionProps) {
- return (
-
-
-
{plan.name}
-
-
{plan.descripton}
-
-
- {plan.price || }
-
-
-
-
- {plan.features.map((feature) => (
- {feature}
- ))}
-
-
-
- {isCurrentPlan ? (
-
This is your current plan
- ) : (
- actionVerb &&
{actionVerb}
- )}
-
-
- );
-}
+import { PlanOption } from "components/PlanOption";
export interface PlanSelectorProps {
plans: Plan[];
@@ -66,15 +15,7 @@ export function PlanSelector({
onPlanSelected,
}: PlanSelectorProps) {
return (
-
+
{plans.map((plan) => (
({
+ path: `/v1/me?returnAccessToken`,
+ });
+
+ const error = meError || userError;
+ if (error) throw error;
+
+ if (!user || !me) return ;
+
+ return (
+
+
+
+
+
+ {getUserAvatarFallback(user)}
+
+
+ {user.name || user.email || "Unknown"}
+ {user.email && {user.email} }
+
+
+
+
+
+
+
+
+
+
+ (window.location.href = "/api/auth/logout?returnTo=/")
+ }
+ >
+ Log out
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/SidebarNav.tsx b/apps/web/src/components/SidebarNav.tsx
new file mode 100644
index 0000000..7334c5a
--- /dev/null
+++ b/apps/web/src/components/SidebarNav.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+import { cn } from "./utils";
+import { buttonVariants } from "components/ui/button";
+
+interface SidebarNavProps extends React.HTMLAttributes {
+ items: {
+ href: string;
+ title: string;
+ }[];
+}
+
+export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
+ const pathname = usePathname();
+
+ return (
+
+ {items.map((item) => (
+
+ {item.title}
+
+ ))}
+
+ );
+}
diff --git a/apps/web/src/components/SuscriptionPlan.tsx b/apps/web/src/components/SuscriptionPlan.tsx
new file mode 100644
index 0000000..32881c5
--- /dev/null
+++ b/apps/web/src/components/SuscriptionPlan.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import { Tenant } from "@letsgo/tenant";
+import { getPlan } from "@letsgo/pricing";
+import { Button } from "components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "components/ui/card";
+import { useRouter } from "next/navigation";
+import { PlanOption } from "./PlanOption";
+
+export interface SubscriptionPlanProps {
+ tenant: Tenant;
+}
+
+export function SubscriptionPlan({ tenant }: SubscriptionPlanProps) {
+ const router = useRouter();
+
+ const plan = getPlan(tenant.plan.planId);
+
+ const handleChangePlan = async () => {
+ router.push(`/manage/${tenant.tenantId}/newplan`);
+ };
+
+ return (
+
+
+ Current plan
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/Team.tsx b/apps/web/src/components/Team.tsx
index bbe0d1a..74d1d29 100644
--- a/apps/web/src/components/Team.tsx
+++ b/apps/web/src/components/Team.tsx
@@ -2,14 +2,25 @@
import { useUser } from "@auth0/nextjs-auth0/client";
import { GetTenantUsersResponse } from "@letsgo/types";
-import { CSSProperties, useEffect, useState } from "react";
-import { useApi, useApiMutate } from "./common-client";
+import { Input } from "components/ui/input";
+import { Label } from "components/ui/label";
+import { Trash2 } from "lucide-react";
+import { useEffect, useState } from "react";
+import { getUserAvatarFallback, useApi, useApiMutate } from "./common-client";
-const style: CSSProperties = {
- border: "1px solid black",
- borderCollapse: "collapse",
- padding: "0.5em",
-};
+import { Avatar, AvatarFallback, AvatarImage } from "components/ui/avatar";
+import { Button } from "components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "components/ui/card";
+import { Popover, PopoverContent, PopoverTrigger } from "components/ui/popover";
+import { CopyButton } from "./CopyButton";
+import { LoadingPlaceholder } from "./LoadingPlaceholder";
+import { ConfirmDialog } from "./ConfirmDialog";
export interface TeamProps {
tenantId: string;
@@ -43,7 +54,6 @@ export function Team({ tenantId }: TeamProps) {
}
}, [deleteIdentity, deleteIdentityId]);
- if (isUsersLoading || isUserLoading) return Loading...
;
const error = usersError || errorDeletingIdentity || userError;
if (error) throw error;
@@ -51,42 +61,128 @@ export function Team({ tenantId }: TeamProps) {
setDeleteIdentityId(identityId);
};
- return (
-
-
-
- Name
- Email
- Issuer
- Subject
- Action
-
-
-
+ return isUsersLoading || isUserLoading ? (
+
+
+ Team Members
+
+
+
+
+
+ ) : (
+
+
+ Team Members
+
+ All team members have full access to the tenant.
+
+
+
{(data?.identities || []).map((identity) => (
-
-
- {identity.user?.name || "N/A"}
- {user?.identityId === identity.identityId ? ` (that's you)` : ``}
-
- {identity.user?.email || "N/A"}
- {identity.iss}
- {identity.sub}
-
- {/** Do not allow the removal of self */}
-
- Remove
-
-
-
+
+
+
+
+
+ {getUserAvatarFallback(identity.user || {})}
+
+
+
+
+
+
+
+ {identity.user?.name || "Unknown"}
+ {user?.identityId === identity.identityId
+ ? ` (that's you)`
+ : ``}
+
+
+
+
+
+
+
+ {identity.user?.name || "Unknown"}
+
+
+ User's identity
+
+
+
+
+
+
+ {identity.user?.email && (
+
+ {identity.user?.email}
+
+ )}
+
+
+
+
+
+ }
+ confirm={Remove }
+ onConfirm={handleRemove(identity.identityId)}
+ />
+
))}
-
-
+
+
);
}
diff --git a/apps/web/src/components/TenantSelector.tsx b/apps/web/src/components/TenantSelector.tsx
index 45062c3..e489489 100644
--- a/apps/web/src/components/TenantSelector.tsx
+++ b/apps/web/src/components/TenantSelector.tsx
@@ -1,10 +1,30 @@
"use client";
-import { useTenant } from "./TenantProvider";
-import { useCallback, useState } from "react";
+import {
+ CaretSortIcon,
+ CheckIcon,
+ PlusCircledIcon,
+} from "@radix-ui/react-icons";
+
+import { Button } from "components/ui/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator,
+} from "components/ui/command";
+import { Popover, PopoverContent, PopoverTrigger } from "components/ui/popover";
+import { cn } from "components/utils";
+import { Loader2, Factory } from "lucide-react";
+
+import { Tenant } from "@letsgo/tenant";
import { useRouter } from "next/navigation";
+import { useCallback, useState } from "react";
+import { useTenant } from "./TenantProvider";
import { useApiMutate } from "./common-client";
-import { Tenant } from "@letsgo/tenant";
const createValue = "create";
@@ -13,6 +33,7 @@ export interface TenantSelectorProps {
}
export function TenantSelector({ allowCreate = false }: TenantSelectorProps) {
+ const [open, setOpen] = useState(false);
const {
isLoading: isTenantLoading,
error: tenantsError,
@@ -32,18 +53,17 @@ export function TenantSelector({ allowCreate = false }: TenantSelectorProps) {
afterSuccess: async (newTenant) => {
if (newTenant) {
await refreshTenants();
- router.push(`/manage/${newTenant.tenantId}/settings`);
+ router.push(`/manage/${newTenant.tenantId}/dashboard`);
}
},
});
const handleTenantChange = useCallback(
- async (e: any) => {
- const tenantId = e.target.value;
+ async (tenantId: string) => {
if (tenantId === createValue) {
createTenant();
} else {
- router.push(`/manage/${tenantId}/settings`);
+ router.push(`/manage/${tenantId}/dashboard`);
}
},
[createTenant, router]
@@ -52,26 +72,97 @@ export function TenantSelector({ allowCreate = false }: TenantSelectorProps) {
const error = tenantsError || errorCreatingTenant;
if (error) throw error;
- if (isTenantLoading) {
- return Loading... ;
- }
-
- if (isCreatingTenant) {
- return Creating... ;
- }
-
- if (tenants) {
- return (
-
- {tenants.map((tenant) => (
-
- {tenant.displayName}
-
- ))}
- {allowCreate && -- Create new -- }
-
- );
- }
-
- return ;
+ return (
+
+
+
+
+ {(isTenantLoading || isCreatingTenant) && (
+
+ )}
+ {isTenantLoading ? (
+ "Loading tenants..."
+ ) : isCreatingTenant ? (
+ "Creating tenant..."
+ ) : (
+ <>
+
+ {currentTenant?.displayName}
+ >
+ )}
+
+ {!isTenantLoading && !isCreatingTenant && (
+
+ )}
+
+
+ {tenants && (
+
+
+
+
+ No tenant found.
+
+ {tenants.map((tenant) => (
+ {
+ handleTenantChange(tenant.tenantId);
+ setOpen(false);
+ }}
+ className="text-sm"
+ >
+
+
+
+ {tenant.displayName}
+
+
+
+
+ ))}
+
+
+ {allowCreate && (
+
+
+
+
+ {
+ setOpen(false);
+ handleTenantChange(createValue);
+ }}
+ >
+
+ Create Tenant
+
+
+
+
+ )}
+
+
+ )}
+
+ );
}
diff --git a/apps/web/src/components/User.tsx b/apps/web/src/components/User.tsx
deleted file mode 100644
index feb2cb5..0000000
--- a/apps/web/src/components/User.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-"use client";
-
-import { useUser } from "@auth0/nextjs-auth0/client";
-
-export function User() {
- const { user, error, isLoading } = useUser();
- if (isLoading) return Loading...
;
- if (error) throw error;
-
- return (
-
-
{JSON.stringify(user, null, 2)}
-
- );
-}
diff --git a/apps/web/src/components/UserNav.tsx b/apps/web/src/components/UserNav.tsx
new file mode 100644
index 0000000..9976404
--- /dev/null
+++ b/apps/web/src/components/UserNav.tsx
@@ -0,0 +1,91 @@
+import { Avatar, AvatarFallback, AvatarImage } from "components/ui/avatar";
+import { Button } from "components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "components/ui/dropdown-menu";
+import { useRouter } from "next/navigation";
+import { getUserAvatarFallback } from "./common-client";
+
+import { UserProfile } from "@auth0/nextjs-auth0/client";
+import { Tenant } from "@letsgo/tenant";
+
+export interface UserNaveProps {
+ user: UserProfile;
+ tenant: Tenant;
+}
+
+export function UserNav({ user, tenant }: UserNaveProps) {
+ const router = useRouter();
+
+ return (
+
+
+
+
+
+ {getUserAvatarFallback(user)}
+
+
+
+
+
+
+ {user.name && (
+
+ {user.name || user.email || "No user name"}
+
+ )}
+ {user.name && user.email && (
+
+ {user.email}
+
+ )}
+
+
+
+
+ router.push(`/manage/${tenant.tenantId}/dashboard`)}
+ >
+ Dashboard
+
+ router.push("/manage/profile")}>
+ Profile
+
+ router.push(`/manage/${tenant.tenantId}/team`)}
+ >
+ Team
+
+
+ router.push(`/manage/${tenant.tenantId}/subscription`)
+ }
+ >
+ Subscription
+
+ router.push("/contact?context=help")}
+ >
+ Help
+
+
+
+ (window.location.href = "/api/auth/logout?returnTo=/")}
+ >
+ Log out
+
+
+
+ );
+}
diff --git a/apps/web/src/components/common-client.tsx b/apps/web/src/components/common-client.tsx
index 71d9d74..e53b164 100644
--- a/apps/web/src/components/common-client.tsx
+++ b/apps/web/src/components/common-client.tsx
@@ -1,10 +1,24 @@
"use client";
+import { UserProfile } from "@auth0/nextjs-auth0/client";
import useSWR, { KeyedMutator } from "swr";
import useSWRMutate from "swr/mutation";
const CurrentTenantKey = "LetsGoCurrentTenant";
+export function getUserAvatarFallback(user: UserProfile) {
+ if (user.name) {
+ return user.name
+ .split(" ")
+ .map((n) => n[0].toUpperCase())
+ .join("");
+ }
+ if (user.nickname) {
+ return user.nickname[0].toUpperCase();
+ }
+ return "U";
+}
+
export function createAbsoluteUrl(relativeUrl: string) {
return `${window.location.protocol}//${window.location.host}${relativeUrl}`;
}
diff --git a/apps/web/src/components/ui/accordion.tsx b/apps/web/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..0ae2502
--- /dev/null
+++ b/apps/web/src/components/ui/accordion.tsx
@@ -0,0 +1,58 @@
+"use client"
+
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "components/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/apps/web/src/components/ui/alert-dialog.tsx b/apps/web/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..624f456
--- /dev/null
+++ b/apps/web/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "components/utils"
+import { buttonVariants } from "components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/apps/web/src/components/ui/alert.tsx b/apps/web/src/components/ui/alert.tsx
new file mode 100644
index 0000000..1f93c34
--- /dev/null
+++ b/apps/web/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "components/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/apps/web/src/components/ui/aspect-ratio.tsx b/apps/web/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..d6a5226
--- /dev/null
+++ b/apps/web/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..9983dda
--- /dev/null
+++ b/apps/web/src/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "components/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx
new file mode 100644
index 0000000..26318f8
--- /dev/null
+++ b/apps/web/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "components/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx
new file mode 100644
index 0000000..e979670
--- /dev/null
+++ b/apps/web/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "components/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ 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,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/apps/web/src/components/ui/calendar.tsx b/apps/web/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..f20f9eb
--- /dev/null
+++ b/apps/web/src/components/ui/calendar.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "components/utils"
+import { buttonVariants } from "components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx
new file mode 100644
index 0000000..fd91292
--- /dev/null
+++ b/apps/web/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "components/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/apps/web/src/components/ui/checkbox.tsx b/apps/web/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..06b62b1
--- /dev/null
+++ b/apps/web/src/components/ui/checkbox.tsx
@@ -0,0 +1,30 @@
+"use client"
+
+import * as React from "react"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { Check } from "lucide-react"
+
+import { cn } from "components/utils"
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+))
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
+
+export { Checkbox }
diff --git a/apps/web/src/components/ui/collapsible.tsx b/apps/web/src/components/ui/collapsible.tsx
new file mode 100644
index 0000000..9fa4894
--- /dev/null
+++ b/apps/web/src/components/ui/collapsible.tsx
@@ -0,0 +1,11 @@
+"use client"
+
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+const Collapsible = CollapsiblePrimitive.Root
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
+
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx
new file mode 100644
index 0000000..513aacb
--- /dev/null
+++ b/apps/web/src/components/ui/command.tsx
@@ -0,0 +1,155 @@
+"use client"
+
+import * as React from "react"
+import { type DialogProps } from "@radix-ui/react-dialog"
+import { Command as CommandPrimitive } from "cmdk"
+import { Search } from "lucide-react"
+
+import { cn } from "components/utils"
+import { Dialog, DialogContent } from "components/ui/dialog"
+
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Command.displayName = CommandPrimitive.displayName
+
+interface CommandDialogProps extends DialogProps {}
+
+const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+const CommandInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+
+CommandInput.displayName = CommandPrimitive.Input.displayName
+
+const CommandList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandList.displayName = CommandPrimitive.List.displayName
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => (
+
+))
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName
+
+const CommandSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName
+
+const CommandItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandItem.displayName = CommandPrimitive.Item.displayName
+
+const CommandShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+CommandShortcut.displayName = "CommandShortcut"
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+}
diff --git a/apps/web/src/components/ui/context-menu.tsx b/apps/web/src/components/ui/context-menu.tsx
new file mode 100644
index 0000000..0e2a8ed
--- /dev/null
+++ b/apps/web/src/components/ui/context-menu.tsx
@@ -0,0 +1,200 @@
+"use client"
+
+import * as React from "react"
+import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "components/utils"
+
+const ContextMenu = ContextMenuPrimitive.Root
+
+const ContextMenuTrigger = ContextMenuPrimitive.Trigger
+
+const ContextMenuGroup = ContextMenuPrimitive.Group
+
+const ContextMenuPortal = ContextMenuPrimitive.Portal
+
+const ContextMenuSub = ContextMenuPrimitive.Sub
+
+const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
+
+const ContextMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
+
+const ContextMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
+
+const ContextMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
+
+const ContextMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
+
+const ContextMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+ContextMenuCheckboxItem.displayName =
+ ContextMenuPrimitive.CheckboxItem.displayName
+
+const ContextMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
+
+const ContextMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
+
+const ContextMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
+
+const ContextMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+ContextMenuShortcut.displayName = "ContextMenuShortcut"
+
+export {
+ ContextMenu,
+ ContextMenuTrigger,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuCheckboxItem,
+ ContextMenuRadioItem,
+ ContextMenuLabel,
+ ContextMenuSeparator,
+ ContextMenuShortcut,
+ ContextMenuGroup,
+ ContextMenuPortal,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
+ ContextMenuRadioGroup,
+}
diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..f9b764e
--- /dev/null
+++ b/apps/web/src/components/ui/dialog.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+import { cn } from "components/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..4b84131
--- /dev/null
+++ b/apps/web/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,200 @@
+"use client"
+
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "components/utils"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+}
diff --git a/apps/web/src/components/ui/form.tsx b/apps/web/src/components/ui/form.tsx
new file mode 100644
index 0000000..d0bbf3f
--- /dev/null
+++ b/apps/web/src/components/ui/form.tsx
@@ -0,0 +1,176 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+} from "react-hook-form"
+
+import { cn } from "components/utils"
+import { Label } from "components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState, formState } = useFormContext()
+
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+)
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId()
+
+ return (
+
+
+
+ )
+})
+FormItem.displayName = "FormItem"
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField()
+
+ return (
+
+ )
+})
+FormLabel.displayName = "FormLabel"
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+})
+FormControl.displayName = "FormControl"
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+})
+FormDescription.displayName = "FormDescription"
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message) : children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+})
+FormMessage.displayName = "FormMessage"
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/apps/web/src/components/ui/hover-card.tsx b/apps/web/src/components/ui/hover-card.tsx
new file mode 100644
index 0000000..ff923f1
--- /dev/null
+++ b/apps/web/src/components/ui/hover-card.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import * as React from "react"
+import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
+
+import { cn } from "components/utils"
+
+const HoverCard = HoverCardPrimitive.Root
+
+const HoverCardTrigger = HoverCardPrimitive.Trigger
+
+const HoverCardContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+))
+HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
+
+export { HoverCard, HoverCardTrigger, HoverCardContent }
diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx
new file mode 100644
index 0000000..d3f53d6
--- /dev/null
+++ b/apps/web/src/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react"
+
+import { cn } from "components/utils"
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx
new file mode 100644
index 0000000..40679cc
--- /dev/null
+++ b/apps/web/src/components/ui/label.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "components/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/apps/web/src/components/ui/menubar.tsx b/apps/web/src/components/ui/menubar.tsx
new file mode 100644
index 0000000..a18727e
--- /dev/null
+++ b/apps/web/src/components/ui/menubar.tsx
@@ -0,0 +1,236 @@
+"use client"
+
+import * as React from "react"
+import * as MenubarPrimitive from "@radix-ui/react-menubar"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "components/utils"
+
+const MenubarMenu = MenubarPrimitive.Menu
+
+const MenubarGroup = MenubarPrimitive.Group
+
+const MenubarPortal = MenubarPrimitive.Portal
+
+const MenubarSub = MenubarPrimitive.Sub
+
+const MenubarRadioGroup = MenubarPrimitive.RadioGroup
+
+const Menubar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Menubar.displayName = MenubarPrimitive.Root.displayName
+
+const MenubarTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
+
+const MenubarSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
+
+const MenubarSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
+
+const MenubarContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
+ ref
+ ) => (
+
+
+
+ )
+)
+MenubarContent.displayName = MenubarPrimitive.Content.displayName
+
+const MenubarItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+MenubarItem.displayName = MenubarPrimitive.Item.displayName
+
+const MenubarCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
+
+const MenubarRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
+
+const MenubarLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+MenubarLabel.displayName = MenubarPrimitive.Label.displayName
+
+const MenubarSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
+
+const MenubarShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+MenubarShortcut.displayname = "MenubarShortcut"
+
+export {
+ Menubar,
+ MenubarMenu,
+ MenubarTrigger,
+ MenubarContent,
+ MenubarItem,
+ MenubarSeparator,
+ MenubarLabel,
+ MenubarCheckboxItem,
+ MenubarRadioGroup,
+ MenubarRadioItem,
+ MenubarPortal,
+ MenubarSubContent,
+ MenubarSubTrigger,
+ MenubarGroup,
+ MenubarSub,
+ MenubarShortcut,
+}
diff --git a/apps/web/src/components/ui/navigation-menu.tsx b/apps/web/src/components/ui/navigation-menu.tsx
new file mode 100644
index 0000000..c77c63c
--- /dev/null
+++ b/apps/web/src/components/ui/navigation-menu.tsx
@@ -0,0 +1,128 @@
+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 "components/utils"
+
+const NavigationMenu = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
+
+const NavigationMenuList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+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-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
+)
+
+const NavigationMenuTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}{" "}
+
+
+))
+NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
+
+const NavigationMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
+
+const NavigationMenuLink = NavigationMenuPrimitive.Link
+
+const NavigationMenuViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+NavigationMenuViewport.displayName =
+ NavigationMenuPrimitive.Viewport.displayName
+
+const NavigationMenuIndicator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+NavigationMenuIndicator.displayName =
+ NavigationMenuPrimitive.Indicator.displayName
+
+export {
+ navigationMenuTriggerStyle,
+ NavigationMenu,
+ NavigationMenuList,
+ NavigationMenuItem,
+ NavigationMenuContent,
+ NavigationMenuTrigger,
+ NavigationMenuLink,
+ NavigationMenuIndicator,
+ NavigationMenuViewport,
+}
diff --git a/apps/web/src/components/ui/popover.tsx b/apps/web/src/components/ui/popover.tsx
new file mode 100644
index 0000000..62f89e8
--- /dev/null
+++ b/apps/web/src/components/ui/popover.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+
+import { cn } from "components/utils"
+
+const Popover = PopoverPrimitive.Root
+
+const PopoverTrigger = PopoverPrimitive.Trigger
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+PopoverContent.displayName = PopoverPrimitive.Content.displayName
+
+export { Popover, PopoverTrigger, PopoverContent }
diff --git a/apps/web/src/components/ui/progress.tsx b/apps/web/src/components/ui/progress.tsx
new file mode 100644
index 0000000..f62ef69
--- /dev/null
+++ b/apps/web/src/components/ui/progress.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as ProgressPrimitive from "@radix-ui/react-progress"
+
+import { cn } from "components/utils"
+
+const Progress = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, value, ...props }, ref) => (
+
+
+
+))
+Progress.displayName = ProgressPrimitive.Root.displayName
+
+export { Progress }
diff --git a/apps/web/src/components/ui/radio-group.tsx b/apps/web/src/components/ui/radio-group.tsx
new file mode 100644
index 0000000..3c69843
--- /dev/null
+++ b/apps/web/src/components/ui/radio-group.tsx
@@ -0,0 +1,44 @@
+"use client"
+
+import * as React from "react"
+import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
+import { Circle } from "lucide-react"
+
+import { cn } from "components/utils"
+
+const RadioGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
+
+const RadioGroupItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+
+
+
+
+ )
+})
+RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
+
+export { RadioGroup, RadioGroupItem }
diff --git a/apps/web/src/components/ui/scroll-area.tsx b/apps/web/src/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..c4c2548
--- /dev/null
+++ b/apps/web/src/components/ui/scroll-area.tsx
@@ -0,0 +1,48 @@
+"use client"
+
+import * as React from "react"
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
+
+import { cn } from "components/utils"
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+))
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+))
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+
+export { ScrollArea, ScrollBar }
diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx
new file mode 100644
index 0000000..92c6ee5
--- /dev/null
+++ b/apps/web/src/components/ui/select.tsx
@@ -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 "components/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/apps/web/src/components/ui/separator.tsx b/apps/web/src/components/ui/separator.tsx
new file mode 100644
index 0000000..14e5172
--- /dev/null
+++ b/apps/web/src/components/ui/separator.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "components/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/apps/web/src/components/ui/sheet.tsx b/apps/web/src/components/ui/sheet.tsx
new file mode 100644
index 0000000..f784f55
--- /dev/null
+++ b/apps/web/src/components/ui/sheet.tsx
@@ -0,0 +1,140 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "components/utils"
+
+const Sheet = SheetPrimitive.Root
+
+const SheetTrigger = SheetPrimitive.Trigger
+
+const SheetClose = SheetPrimitive.Close
+
+const SheetPortal = SheetPrimitive.Portal
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
+
+const sheetVariants = cva(
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ }
+)
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+SheetContent.displayName = SheetPrimitive.Content.displayName
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetHeader.displayName = "SheetHeader"
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetFooter.displayName = "SheetFooter"
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetTitle.displayName = SheetPrimitive.Title.displayName
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetDescription.displayName = SheetPrimitive.Description.displayName
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/apps/web/src/components/ui/skeleton.tsx b/apps/web/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..af02e01
--- /dev/null
+++ b/apps/web/src/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "components/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/apps/web/src/components/ui/slider.tsx b/apps/web/src/components/ui/slider.tsx
new file mode 100644
index 0000000..0bf16dc
--- /dev/null
+++ b/apps/web/src/components/ui/slider.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as SliderPrimitive from "@radix-ui/react-slider"
+
+import { cn } from "components/utils"
+
+const Slider = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+
+))
+Slider.displayName = SliderPrimitive.Root.displayName
+
+export { Slider }
diff --git a/apps/web/src/components/ui/switch.tsx b/apps/web/src/components/ui/switch.tsx
new file mode 100644
index 0000000..f705bfc
--- /dev/null
+++ b/apps/web/src/components/ui/switch.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+
+import { cn } from "components/utils"
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/apps/web/src/components/ui/table.tsx b/apps/web/src/components/ui/table.tsx
new file mode 100644
index 0000000..65e048a
--- /dev/null
+++ b/apps/web/src/components/ui/table.tsx
@@ -0,0 +1,117 @@
+import * as React from "react"
+
+import { cn } from "components/utils"
+
+const Table = React.forwardRef<
+ HTMLTableElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Table.displayName = "Table"
+
+const TableHeader = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableHeader.displayName = "TableHeader"
+
+const TableBody = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableBody.displayName = "TableBody"
+
+const TableFooter = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+ tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+))
+TableFooter.displayName = "TableFooter"
+
+const TableRow = React.forwardRef<
+ HTMLTableRowElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableRow.displayName = "TableRow"
+
+const TableHead = React.forwardRef<
+ HTMLTableCellElement,
+ React.ThHTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableHead.displayName = "TableHead"
+
+const TableCell = React.forwardRef<
+ HTMLTableCellElement,
+ React.TdHTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableCell.displayName = "TableCell"
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableCaption.displayName = "TableCaption"
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/apps/web/src/components/ui/tabs.tsx b/apps/web/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..a7c2f58
--- /dev/null
+++ b/apps/web/src/components/ui/tabs.tsx
@@ -0,0 +1,55 @@
+"use client"
+
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "components/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/apps/web/src/components/ui/textarea.tsx b/apps/web/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..69a525a
--- /dev/null
+++ b/apps/web/src/components/ui/textarea.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+
+import { cn } from "components/utils"
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes {}
+
+const Textarea = React.forwardRef(
+ ({ className, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Textarea.displayName = "Textarea"
+
+export { Textarea }
diff --git a/apps/web/src/components/ui/toast.tsx b/apps/web/src/components/ui/toast.tsx
new file mode 100644
index 0000000..94a84da
--- /dev/null
+++ b/apps/web/src/components/ui/toast.tsx
@@ -0,0 +1,127 @@
+import * as React from "react"
+import * as ToastPrimitives from "@radix-ui/react-toast"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "components/utils"
+
+const ToastProvider = ToastPrimitives.Provider
+
+const ToastViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastViewport.displayName = ToastPrimitives.Viewport.displayName
+
+const toastVariants = cva(
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
+ {
+ variants: {
+ variant: {
+ default: "border bg-background text-foreground",
+ destructive:
+ "destructive group border-destructive bg-destructive text-destructive-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Toast = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps