B2B-88: add starter kit structure and elements
This commit is contained in:
104
components/analytics-provider.tsx
Normal file
104
components/analytics-provider.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { analytics } from '@kit/analytics';
|
||||
import {
|
||||
AppEvent,
|
||||
AppEventType,
|
||||
ConsumerProvidedEventTypes,
|
||||
useAppEvents,
|
||||
} from '@kit/shared/events';
|
||||
import { isBrowser } from '@kit/shared/utils';
|
||||
|
||||
type AnalyticsMapping<
|
||||
T extends ConsumerProvidedEventTypes = NonNullable<unknown>,
|
||||
> = {
|
||||
[K in AppEventType<T>]?: (event: AppEvent<T, K>) => unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to subscribe to app events and map them to analytics actions
|
||||
* @param mapping
|
||||
*/
|
||||
function useAnalyticsMapping<T extends ConsumerProvidedEventTypes>(
|
||||
mapping: AnalyticsMapping<T>,
|
||||
) {
|
||||
const appEvents = useAppEvents<T>();
|
||||
|
||||
useEffect(() => {
|
||||
const subscriptions = Object.entries(mapping).map(
|
||||
([eventType, handler]) => {
|
||||
appEvents.on(eventType as AppEventType<T>, handler);
|
||||
|
||||
return () => appEvents.off(eventType as AppEventType<T>, handler);
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
}, [appEvents, mapping]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a mapping of app events to analytics actions
|
||||
* Add new mappings here to track new events in the analytics service from app events
|
||||
*/
|
||||
const analyticsMapping: AnalyticsMapping = {
|
||||
'user.signedIn': (event) => {
|
||||
const { userId, ...traits } = event.payload;
|
||||
|
||||
if (userId) {
|
||||
return analytics.identify(userId, traits);
|
||||
}
|
||||
},
|
||||
'user.signedUp': (event) => {
|
||||
return analytics.trackEvent(event.type, event.payload);
|
||||
},
|
||||
'checkout.started': (event) => {
|
||||
return analytics.trackEvent(event.type, event.payload);
|
||||
},
|
||||
'user.updated': (event) => {
|
||||
return analytics.trackEvent(event.type, event.payload);
|
||||
},
|
||||
};
|
||||
|
||||
function AnalyticsProviderBrowser(props: React.PropsWithChildren) {
|
||||
// Subscribe to app events and map them to analytics actions
|
||||
useAnalyticsMapping(analyticsMapping);
|
||||
|
||||
// Report page views to the analytics service
|
||||
useReportPageView(useCallback((url) => analytics.trackPageView(url), []));
|
||||
|
||||
// Render children
|
||||
return props.children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider for the analytics service
|
||||
*/
|
||||
export function AnalyticsProvider(props: React.PropsWithChildren) {
|
||||
if (!isBrowser()) {
|
||||
return props.children;
|
||||
}
|
||||
|
||||
return <AnalyticsProviderBrowser>{props.children}</AnalyticsProviderBrowser>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to report page views to the analytics service
|
||||
* @param reportAnalyticsFn
|
||||
*/
|
||||
function useReportPageView(reportAnalyticsFn: (url: string) => unknown) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const url = [pathname, searchParams.toString()].filter(Boolean).join('?');
|
||||
|
||||
reportAnalyticsFn(url);
|
||||
}, [pathname, reportAnalyticsFn, searchParams]);
|
||||
}
|
||||
47
components/app-logo.tsx
Normal file
47
components/app-logo.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
function LogoImage({
|
||||
className,
|
||||
width = 105,
|
||||
}: {
|
||||
className?: string;
|
||||
width?: number;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
className={cn(`w-[80px] lg:w-[95px]`, className)}
|
||||
viewBox="0 0 733 140"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
className={'fill-primary dark:fill-white'}
|
||||
d="M119.081 138V73.209C119.081 67.551 117.08 62.79 113.078 58.926C109.214 55.062 104.453 53.13 98.7951 53.13C93.2751 53.13 88.4451 55.062 84.3051 58.926C80.3031 62.652 78.3021 67.344 78.3021 73.002V138H59.4651V73.002C59.4651 67.206 57.5331 62.514 53.6691 58.926C49.5291 55.062 44.6301 53.13 38.9721 53.13C33.4521 53.13 28.7601 55.062 24.8961 58.926C20.7561 63.066 18.6861 67.965 18.6861 73.623V138H0.0560548V36.984H18.6861V44.643C21.0321 41.745 24.0681 39.33 27.7941 37.398C31.6581 35.466 35.3841 34.5 38.9721 34.5C45.0441 34.5 50.5641 35.742 55.5321 38.226C60.6381 40.572 65.0541 43.884 68.7801 48.162C72.5061 43.884 76.9221 40.572 82.0281 38.226C87.1341 35.742 92.7231 34.5 98.7951 34.5C104.177 34.5 109.214 35.466 113.906 37.398C118.598 39.33 122.738 42.09 126.326 45.678C129.914 49.266 132.674 53.475 134.606 58.305C136.676 62.997 137.711 67.965 137.711 73.209V138H119.081ZM242.173 138V122.268C237.757 127.374 232.651 131.445 226.855 134.481C221.059 137.517 214.918 139.035 208.432 139.035C201.256 139.035 194.494 137.724 188.146 135.102C181.936 132.48 176.416 128.754 171.586 123.924C166.756 119.232 162.961 113.712 160.201 107.364C157.579 100.878 156.268 94.116 156.268 87.078C156.268 80.04 157.579 73.347 160.201 66.999C162.961 60.513 166.756 54.855 171.586 50.025C176.416 45.195 181.936 41.469 188.146 38.847C194.494 36.225 201.256 34.914 208.432 34.914C215.056 34.914 221.266 36.294 227.062 39.054C232.996 41.814 238.033 45.678 242.173 50.646V36.984H260.803V138H242.173ZM208.432 53.337C203.878 53.337 199.462 54.234 195.184 56.028C191.044 57.684 187.456 60.03 184.42 63.066C181.384 66.102 178.969 69.759 177.175 74.037C175.519 78.177 174.691 82.524 174.691 87.078C174.691 91.632 175.519 95.979 177.175 100.119C178.969 104.259 181.384 107.847 184.42 110.883C187.456 113.919 191.044 116.334 195.184 118.128C199.462 119.784 203.878 120.612 208.432 120.612C212.986 120.612 217.333 119.784 221.473 118.128C225.613 116.334 229.201 113.919 232.237 110.883C235.273 107.847 237.619 104.259 239.275 100.119C241.069 95.979 241.966 91.632 241.966 87.078C241.966 82.524 241.069 78.177 239.275 74.037C237.619 69.759 235.273 66.102 232.237 63.066C229.201 60.03 225.613 57.684 221.473 56.028C217.333 54.234 212.986 53.337 208.432 53.337ZM331.127 138L299.663 99.705V138H281.447V0.344996H299.663V59.754L327.815 33.258H354.932L305.873 78.798L355.139 138H331.127ZM379.299 94.116C379.299 97.428 380.472 100.878 382.818 104.466C385.302 108.054 388.131 111.09 391.305 113.574C397.101 118.128 403.863 120.405 411.591 120.405C423.873 120.405 433.878 114.471 441.606 102.603L457.338 111.918C451.956 120.612 445.332 127.305 437.466 131.997C429.6 136.689 420.975 139.035 411.591 139.035C404.553 139.035 397.86 137.724 391.512 135.102C385.164 132.342 379.575 128.547 374.745 123.717C369.915 118.887 366.12 113.298 363.36 106.95C360.738 100.602 359.427 93.909 359.427 86.871C359.427 79.833 360.738 73.14 363.36 66.792C366.12 60.306 369.915 54.648 374.745 49.818C379.437 44.988 384.957 41.262 391.305 38.64C397.791 36.018 404.553 34.707 411.591 34.707C418.629 34.707 425.322 36.018 431.67 38.64C438.156 41.262 443.745 44.988 448.437 49.818C458.649 60.306 463.755 72.45 463.755 86.25C463.755 88.734 463.548 91.356 463.134 94.116H379.299ZM411.591 51.681C405.933 51.681 400.62 52.923 395.652 55.407C390.684 57.891 386.682 61.203 383.646 65.343C380.748 69.345 379.299 73.623 379.299 78.177H443.883C443.883 73.623 442.365 69.345 439.329 65.343C436.431 61.203 432.498 57.891 427.53 55.407C422.562 52.923 417.249 51.681 411.591 51.681ZM528.543 54.372C525.231 52.854 522.264 52.095 519.642 52.095C514.122 52.095 509.568 54.027 505.98 57.891C502.116 62.031 500.184 66.792 500.184 72.174V138H482.382V72.174C482.382 64.722 484.245 57.891 487.971 51.681C491.835 45.471 497.079 40.641 503.703 37.191C508.671 34.845 513.984 33.672 519.642 33.672C524.196 33.672 528.543 34.5 532.683 36.156C536.823 37.812 541.17 40.503 545.724 44.229L528.543 54.372ZM610.092 138L578.628 99.705V138H560.412V0.344996H578.628V59.754L606.78 33.258H633.897L584.838 78.798L634.104 138H610.092ZM656.049 19.596C653.427 19.596 651.15 18.699 649.218 16.905C647.424 14.973 646.527 12.696 646.527 10.074C646.527 7.45199 647.424 5.24399 649.218 3.44999C651.15 1.51799 653.427 0.551993 656.049 0.551993C658.671 0.551993 660.879 1.51799 662.673 3.44999C664.605 5.24399 665.571 7.45199 665.571 10.074C665.571 12.696 664.605 14.973 662.673 16.905C660.879 18.699 658.671 19.596 656.049 19.596ZM647.562 138V34.5H664.95V138H647.562ZM717.4 53.13V138H699.805V53.13H684.28V34.5H699.805V0.344996H717.4V34.5H732.925V53.13H717.4Z"
|
||||
fill="url(#paint0_linear_1666_2)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppLogo({
|
||||
href,
|
||||
label,
|
||||
className,
|
||||
}: {
|
||||
href?: string | null;
|
||||
className?: string;
|
||||
label?: string;
|
||||
}) {
|
||||
if (href === null) {
|
||||
return <LogoImage className={className} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link aria-label={label ?? 'Home Page'} href={href ?? '/'} prefetch={true}>
|
||||
<LogoImage className={className} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
79
components/auth-provider.tsx
Normal file
79
components/auth-provider.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { AuthChangeEvent, Session } from '@supabase/supabase-js';
|
||||
|
||||
import { useMonitoring } from '@kit/monitoring/hooks';
|
||||
import { useAppEvents } from '@kit/shared/events';
|
||||
import { useAuthChangeListener } from '@kit/supabase/hooks/use-auth-change-listener';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
export function AuthProvider(props: React.PropsWithChildren) {
|
||||
const dispatchEvent = useDispatchAppEventFromAuthEvent();
|
||||
|
||||
const onEvent = useCallback(
|
||||
(event: AuthChangeEvent, session: Session | null) => {
|
||||
dispatchEvent(event, session?.user.id, {
|
||||
email: session?.user.email ?? '',
|
||||
});
|
||||
},
|
||||
[dispatchEvent],
|
||||
);
|
||||
|
||||
useAuthChangeListener({
|
||||
appHomePath: pathsConfig.app.home,
|
||||
onEvent,
|
||||
});
|
||||
|
||||
return props.children;
|
||||
}
|
||||
|
||||
function useDispatchAppEventFromAuthEvent() {
|
||||
const { emit } = useAppEvents();
|
||||
const monitoring = useMonitoring();
|
||||
|
||||
return useCallback(
|
||||
(
|
||||
type: AuthChangeEvent,
|
||||
userId: string | undefined,
|
||||
traits: Record<string, string> = {},
|
||||
) => {
|
||||
switch (type) {
|
||||
case 'INITIAL_SESSION':
|
||||
if (userId) {
|
||||
emit({
|
||||
type: 'user.signedIn',
|
||||
payload: { userId, ...traits },
|
||||
});
|
||||
|
||||
monitoring.identifyUser({ id: userId, ...traits });
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'SIGNED_IN':
|
||||
if (userId) {
|
||||
emit({
|
||||
type: 'user.signedIn',
|
||||
payload: { userId, ...traits },
|
||||
});
|
||||
|
||||
monitoring.identifyUser({ id: userId, ...traits });
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'USER_UPDATED':
|
||||
emit({
|
||||
type: 'user.updated',
|
||||
payload: { userId: userId!, ...traits },
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
},
|
||||
[emit, monitoring],
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export default function DeployButton() {
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&project-name=nextjs-with-supabase&repository-name=nextjs-with-supabase&demo-title=nextjs-with-supabase&demo-description=This+starter+configures+Supabase+Auth+to+use+cookies%2C+making+the+user%27s+session+available+throughout+the+entire+Next.js+app+-+Client+Components%2C+Server+Components%2C+Route+Handlers%2C+Server+Actions+and+Middleware.&demo-url=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2F&external-id=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&demo-image=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2Fopengraph-image.png"
|
||||
target="_blank"
|
||||
>
|
||||
<Button className="flex items-center gap-2" size={"sm"}>
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
viewBox="0 0 76 65"
|
||||
fill="hsl(var(--background)/1)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" fill="inherit" />
|
||||
</svg>
|
||||
<span>Deploy to Vercel</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export function EnvVarWarning() {
|
||||
return (
|
||||
<div className="flex gap-4 items-center">
|
||||
<Badge variant={"outline"} className="font-normal">
|
||||
Supabase environment variables required
|
||||
</Badge>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant={"outline"}
|
||||
disabled
|
||||
className="opacity-75 cursor-none pointer-events-none"
|
||||
>
|
||||
<Link href="/sign-in">Sign in</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant={"default"}
|
||||
disabled
|
||||
className="opacity-75 cursor-none pointer-events-none"
|
||||
>
|
||||
<Link href="/sign-up">Sign up</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
export type Message =
|
||||
| { success: string }
|
||||
| { error: string }
|
||||
| { message: string };
|
||||
|
||||
export function FormMessage({ message }: { message: Message }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full max-w-md text-sm">
|
||||
{"success" in message && (
|
||||
<div className="text-foreground border-l-2 border-foreground px-4">
|
||||
{message.success}
|
||||
</div>
|
||||
)}
|
||||
{"error" in message && (
|
||||
<div className="text-destructive-foreground border-l-2 border-destructive-foreground px-4">
|
||||
{message.error}
|
||||
</div>
|
||||
)}
|
||||
{"message" in message && (
|
||||
<div className="text-foreground border-l-2 px-4">{message.message}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { signOutAction } from "@/app/actions";
|
||||
import { hasEnvVars } from "@/utils/supabase/check-env-vars";
|
||||
import Link from "next/link";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Button } from "./ui/button";
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
|
||||
export default async function AuthButton() {
|
||||
const supabase = await createClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (!hasEnvVars) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-4 items-center">
|
||||
<div>
|
||||
<Badge
|
||||
variant={"default"}
|
||||
className="font-normal pointer-events-none"
|
||||
>
|
||||
Please update .env.local file with anon key and url
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant={"outline"}
|
||||
disabled
|
||||
className="opacity-75 cursor-none pointer-events-none"
|
||||
>
|
||||
<Link href="/sign-in">Sign in</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant={"default"}
|
||||
disabled
|
||||
className="opacity-75 cursor-none pointer-events-none"
|
||||
>
|
||||
<Link href="/sign-up">Sign up</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return user ? (
|
||||
<div className="flex items-center gap-4">
|
||||
Hey, {user.email}!
|
||||
<form action={signOutAction}>
|
||||
<Button type="submit" variant={"outline"}>
|
||||
Sign out
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<Button asChild size="sm" variant={"outline"}>
|
||||
<Link href="/sign-in">Sign in</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant={"default"}>
|
||||
<Link href="/sign-up">Sign up</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import NextLogo from "./next-logo";
|
||||
import SupabaseLogo from "./supabase-logo";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div className="flex flex-col gap-16 items-center">
|
||||
<div className="flex gap-8 justify-center items-center">
|
||||
<a
|
||||
href="https://supabase.com/?utm_source=create-next-app&utm_medium=template&utm_term=nextjs"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<SupabaseLogo />
|
||||
</a>
|
||||
<span className="border-l rotate-45 h-6" />
|
||||
<a href="https://nextjs.org/" target="_blank" rel="noreferrer">
|
||||
<NextLogo />
|
||||
</a>
|
||||
</div>
|
||||
<h1 className="sr-only">Supabase and Next.js Starter Template</h1>
|
||||
<p className="text-3xl lg:text-4xl !leading-tight mx-auto max-w-xl text-center">
|
||||
The fastest way to build apps with{" "}
|
||||
<a
|
||||
href="https://supabase.com/?utm_source=create-next-app&utm_medium=template&utm_term=nextjs"
|
||||
target="_blank"
|
||||
className="font-bold hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Supabase
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
href="https://nextjs.org/"
|
||||
target="_blank"
|
||||
className="font-bold hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Next.js
|
||||
</a>
|
||||
</p>
|
||||
<div className="w-full p-[1px] bg-gradient-to-r from-transparent via-foreground/10 to-transparent my-8" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
export default function NextLogo() {
|
||||
return (
|
||||
<svg
|
||||
aria-label="Next.js logotype"
|
||||
height="68"
|
||||
role="img"
|
||||
viewBox="0 0 394 79"
|
||||
width="100"
|
||||
>
|
||||
<path
|
||||
d="M261.919 0.0330722H330.547V12.7H303.323V79.339H289.71V12.7H261.919V0.0330722Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M149.052 0.0330722V12.7H94.0421V33.0772H138.281V45.7441H94.0421V66.6721H149.052V79.339H80.43V12.7H80.4243V0.0330722H149.052Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M183.32 0.0661486H165.506L229.312 79.3721H247.178L215.271 39.7464L247.127 0.126654L229.312 0.154184L206.352 28.6697L183.32 0.0661486Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M201.6 56.7148L192.679 45.6229L165.455 79.4326H183.32L201.6 56.7148Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M80.907 79.339L17.0151 0H0V79.3059H13.6121V16.9516L63.8067 79.339H80.907Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M333.607 78.8546C332.61 78.8546 331.762 78.5093 331.052 77.8186C330.342 77.1279 329.991 76.2917 330 75.3011C329.991 74.3377 330.342 73.5106 331.052 72.8199C331.762 72.1292 332.61 71.7838 333.607 71.7838C334.566 71.7838 335.405 72.1292 336.115 72.8199C336.835 73.5106 337.194 74.3377 337.204 75.3011C337.194 75.9554 337.028 76.5552 336.696 77.0914C336.355 77.6368 335.922 78.064 335.377 78.373C334.842 78.6911 334.252 78.8546 333.607 78.8546Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M356.84 45.4453H362.872V68.6846C362.863 70.8204 362.401 72.6472 361.498 74.1832C360.585 75.7191 359.321 76.8914 357.698 77.7185C356.084 78.5364 354.193 78.9546 352.044 78.9546C350.079 78.9546 348.318 78.6001 346.75 77.9094C345.182 77.2187 343.937 76.1826 343.024 74.8193C342.101 73.456 341.649 71.7565 341.649 69.7207H347.691C347.7 70.6114 347.903 71.3838 348.29 72.0291C348.677 72.6744 349.212 73.1651 349.895 73.5105C350.586 73.8559 351.38 74.0286 352.274 74.0286C353.243 74.0286 354.073 73.8286 354.746 73.4196C355.419 73.0197 355.936 72.4199 356.296 71.6201C356.646 70.8295 356.831 69.8479 356.84 68.6846V45.4453Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M387.691 54.5338C387.544 53.1251 386.898 52.0254 385.773 51.2438C384.638 50.4531 383.172 50.0623 381.373 50.0623C380.11 50.0623 379.022 50.2532 378.118 50.6258C377.214 51.0075 376.513 51.5164 376.033 52.1617C375.554 52.807 375.314 53.5432 375.295 54.3703C375.295 55.061 375.461 55.6608 375.784 56.1607C376.107 56.6696 376.54 57.0968 377.103 57.4422C377.656 57.7966 378.274 58.0874 378.948 58.3237C379.63 58.56 380.313 58.76 380.995 58.9236L384.14 59.6961C385.404 59.9869 386.631 60.3778 387.802 60.8776C388.973 61.3684 390.034 61.9955 390.965 62.7498C391.897 63.5042 392.635 64.413 393.179 65.4764C393.723 66.5397 394 67.7848 394 69.2208C394 71.1566 393.502 72.8562 392.496 74.3285C391.491 75.7917 390.043 76.9369 388.143 77.764C386.252 78.582 383.965 79 381.272 79C378.671 79 376.402 78.6002 374.493 77.8004C372.575 77.0097 371.08 75.8463 370.001 74.3194C368.922 72.7926 368.341 70.9294 368.258 68.7391H374.235C374.318 69.8842 374.687 70.8386 375.314 71.6111C375.95 72.3745 376.78 72.938 377.795 73.3197C378.819 73.6923 379.962 73.8832 381.226 73.8832C382.545 73.8832 383.707 73.6832 384.712 73.2924C385.708 72.9016 386.492 72.3564 387.055 71.6475C387.627 70.9476 387.913 70.1206 387.922 69.1754C387.913 68.312 387.654 67.5939 387.156 67.0304C386.649 66.467 385.948 65.9944 385.053 65.6127C384.15 65.231 383.098 64.8856 381.899 64.5857L378.081 63.6223C375.323 62.9225 373.137 61.8592 371.541 60.4323C369.937 59.0054 369.143 57.115 369.143 54.7429C369.143 52.798 369.678 51.0894 370.758 49.6261C371.827 48.1629 373.294 47.0268 375.148 46.2179C377.011 45.4 379.114 45 381.456 45C383.836 45 385.92 45.4 387.719 46.2179C389.517 47.0268 390.929 48.1538 391.952 49.5897C392.976 51.0257 393.511 52.6707 393.539 54.5338H387.691Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
49
components/personal-account-dropdown-container.tsx
Normal file
49
components/personal-account-dropdown-container.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
'use client';
|
||||
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
|
||||
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
|
||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||
|
||||
import featuresFlagConfig from '~/config/feature-flags.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
const paths = {
|
||||
home: pathsConfig.app.home,
|
||||
};
|
||||
|
||||
const features = {
|
||||
enableThemeToggle: featuresFlagConfig.enableThemeToggle,
|
||||
};
|
||||
|
||||
export function ProfileAccountDropdownContainer(props: {
|
||||
user?: User;
|
||||
showProfileName?: boolean;
|
||||
|
||||
account?: {
|
||||
id: string | null;
|
||||
name: string | null;
|
||||
picture_url: string | null;
|
||||
};
|
||||
}) {
|
||||
const signOut = useSignOut();
|
||||
const user = useUser(props.user);
|
||||
const userData = user.data;
|
||||
|
||||
if (!userData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PersonalAccountDropdown
|
||||
className={'w-full'}
|
||||
paths={paths}
|
||||
features={features}
|
||||
user={userData}
|
||||
account={props.account}
|
||||
signOutRequested={() => signOut.mutateAsync()}
|
||||
showProfileName={props.showProfileName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
components/react-query-provider.tsx
Normal file
26
components/react-query-provider.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
export function ReactQueryProvider(props: React.PropsWithChildren) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// With SSR, we usually want to set some default staleTime
|
||||
// above 0 to avoid refetching immediately on the client
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{props.children}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
89
components/root-providers.tsx
Normal file
89
components/root-providers.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
|
||||
import { CaptchaProvider } from '@kit/auth/captcha/client';
|
||||
import { I18nProvider } from '@kit/i18n/provider';
|
||||
import { MonitoringProvider } from '@kit/monitoring/components';
|
||||
import { AppEventsProvider } from '@kit/shared/events';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { VersionUpdater } from '@kit/ui/version-updater';
|
||||
|
||||
import { AnalyticsProvider } from '~/components/analytics-provider';
|
||||
import { AuthProvider } from '~/components/auth-provider';
|
||||
import appConfig from '~/config/app.config';
|
||||
import authConfig from '~/config/auth.config';
|
||||
import featuresFlagConfig from '~/config/feature-flags.config';
|
||||
import { i18nResolver } from '~/lib/i18n/i18n.resolver';
|
||||
import { getI18nSettings } from '~/lib/i18n/i18n.settings';
|
||||
|
||||
import { ReactQueryProvider } from './react-query-provider';
|
||||
|
||||
const captchaSiteKey = authConfig.captchaTokenSiteKey;
|
||||
|
||||
const CaptchaTokenSetter = dynamic(async () => {
|
||||
if (!captchaSiteKey) {
|
||||
return Promise.resolve(() => null);
|
||||
}
|
||||
|
||||
const { CaptchaTokenSetter } = await import('@kit/auth/captcha/client');
|
||||
|
||||
return {
|
||||
default: CaptchaTokenSetter,
|
||||
};
|
||||
});
|
||||
|
||||
type RootProvidersProps = React.PropsWithChildren<{
|
||||
// The language to use for the app (optional)
|
||||
lang?: string;
|
||||
// The theme (light or dark or system) (optional)
|
||||
theme?: string;
|
||||
// The CSP nonce to pass to scripts (optional)
|
||||
nonce?: string;
|
||||
}>;
|
||||
|
||||
export function RootProviders({
|
||||
lang,
|
||||
theme = appConfig.theme,
|
||||
nonce,
|
||||
children,
|
||||
}: RootProvidersProps) {
|
||||
const i18nSettings = useMemo(() => getI18nSettings(lang), [lang]);
|
||||
|
||||
return (
|
||||
<MonitoringProvider>
|
||||
<AppEventsProvider>
|
||||
<AnalyticsProvider>
|
||||
<ReactQueryProvider>
|
||||
<I18nProvider settings={i18nSettings} resolver={i18nResolver}>
|
||||
<CaptchaProvider>
|
||||
<CaptchaTokenSetter siteKey={captchaSiteKey} />
|
||||
|
||||
<AuthProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
defaultTheme={theme}
|
||||
enableColorScheme={false}
|
||||
nonce={nonce}
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
</CaptchaProvider>
|
||||
|
||||
<If condition={featuresFlagConfig.enableVersionUpdater}>
|
||||
<VersionUpdater />
|
||||
</If>
|
||||
</I18nProvider>
|
||||
</ReactQueryProvider>
|
||||
</AnalyticsProvider>
|
||||
</AppEventsProvider>
|
||||
</MonitoringProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { type ComponentProps } from "react";
|
||||
import { useFormStatus } from "react-dom";
|
||||
|
||||
type Props = ComponentProps<typeof Button> & {
|
||||
pendingText?: string;
|
||||
};
|
||||
|
||||
export function SubmitButton({
|
||||
children,
|
||||
pendingText = "Submitting...",
|
||||
...props
|
||||
}: Props) {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button type="submit" aria-disabled={pending} {...props}>
|
||||
{pending ? pendingText : children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
export default function SupabaseLogo() {
|
||||
return (
|
||||
<svg
|
||||
aria-label="Supabase logo"
|
||||
width="140"
|
||||
height="30"
|
||||
viewBox="0 0 115 23"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_4671_51136)">
|
||||
<g clipPath="url(#clip1_4671_51136)">
|
||||
<path
|
||||
d="M13.4028 21.8652C12.8424 22.5629 11.7063 22.1806 11.6928 21.2898L11.4954 8.25948H20.3564C21.9614 8.25948 22.8565 10.0924 21.8585 11.3353L13.4028 21.8652Z"
|
||||
fill="url(#paint0_linear_4671_51136)"
|
||||
/>
|
||||
<path
|
||||
d="M13.4028 21.8652C12.8424 22.5629 11.7063 22.1806 11.6928 21.2898L11.4954 8.25948H20.3564C21.9614 8.25948 22.8565 10.0924 21.8585 11.3353L13.4028 21.8652Z"
|
||||
fill="url(#paint1_linear_4671_51136)"
|
||||
fillOpacity="0.2"
|
||||
/>
|
||||
<path
|
||||
d="M9.79895 0.89838C10.3593 0.200591 11.4954 0.582929 11.5089 1.47383L11.5955 14.5041H2.84528C1.24026 14.5041 0.345103 12.6711 1.34316 11.4283L9.79895 0.89838Z"
|
||||
fill="#3ECF8E"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M30.5894 13.3913C30.7068 14.4766 31.7052 16.3371 34.6026 16.3371C37.1279 16.3371 38.3418 14.7479 38.3418 13.1976C38.3418 11.8022 37.3824 10.6588 35.4836 10.2712L34.1131 9.98049C33.5846 9.88359 33.2323 9.5929 33.2323 9.12777C33.2323 8.58512 33.7804 8.17818 34.4656 8.17818C35.5618 8.17818 35.9729 8.89521 36.0513 9.45725L38.2243 8.97275C38.1069 7.94561 37.1867 6.22083 34.446 6.22083C32.3709 6.22083 30.844 7.63555 30.844 9.34094C30.844 10.6781 31.6856 11.7828 33.5454 12.1898L34.8179 12.4805C35.5618 12.6355 35.8555 12.9844 35.8555 13.4107C35.8555 13.9146 35.4444 14.3603 34.583 14.3603C33.4476 14.3603 32.8797 13.6626 32.8212 12.9068L30.5894 13.3913Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M46.6623 16.0464H49.1486C49.1094 15.717 49.0506 15.0581 49.0506 14.3216V6.51154H46.4468V12.0542C46.4468 13.1588 45.7813 13.934 44.6263 13.934C43.4126 13.934 42.8643 13.0813 42.8643 12.0154V6.51154H40.2606V12.5387C40.2606 14.6123 41.5918 16.2984 43.9215 16.2984C44.9393 16.2984 46.0556 15.9108 46.5841 15.0193C46.5841 15.4069 46.6231 15.8526 46.6623 16.0464Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M54.433 19.7286V15.1162C54.9027 15.7558 55.8817 16.279 57.213 16.279C59.9341 16.279 61.7545 14.1472 61.7545 11.2596C61.7545 8.43021 60.1298 6.29842 57.3108 6.29842C55.8623 6.29842 54.7855 6.93792 54.3548 7.67439V6.51159H51.8295V19.7286H54.433ZM59.19 11.279C59.19 12.9845 58.133 13.9728 56.8017 13.9728C55.4708 13.9728 54.394 12.9651 54.394 11.279C54.394 9.59299 55.4708 8.6046 56.8017 8.6046C58.133 8.6046 59.19 9.59299 59.19 11.279Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M63.229 13.4495C63.229 14.9417 64.4818 16.3177 66.5375 16.3177C67.9662 16.3177 68.8865 15.6588 69.3758 14.9029C69.3758 15.2712 69.4149 15.7944 69.4737 16.0464H71.862C71.8033 15.7169 71.7449 15.0386 71.7449 14.5348V9.84482C71.7449 7.92622 70.6093 6.22083 67.5555 6.22083C64.9713 6.22083 63.5811 7.86807 63.4248 9.36033L65.7347 9.84482C65.8131 9.0115 66.4395 8.29445 67.5747 8.29445C68.6713 8.29445 69.1998 8.85646 69.1998 9.53475C69.1998 9.86421 69.0238 10.1355 68.4755 10.2131L66.1068 10.5619C64.5015 10.7945 63.229 11.744 63.229 13.4495ZM67.0854 14.3991C66.2438 14.3991 65.8325 13.8565 65.8325 13.2945C65.8325 12.558 66.361 12.1898 67.0268 12.0929L69.1998 11.7634V12.1898C69.1998 13.8759 68.1818 14.3991 67.0854 14.3991Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M76.895 16.0465V14.8837C77.4038 15.6976 78.4217 16.279 79.7531 16.279C82.4941 16.279 84.2951 14.1278 84.2951 11.2403C84.2951 8.4108 82.6701 6.25965 79.851 6.25965C78.4217 6.25965 77.3648 6.8798 76.934 7.55806V2.01546H74.3696V16.0465H76.895ZM81.6911 11.2596C81.6911 13.0038 80.6341 13.9728 79.3028 13.9728C77.9912 13.9728 76.895 12.9845 76.895 11.2596C76.895 9.51543 77.9912 8.56584 79.3028 8.56584C80.6341 8.56584 81.6911 9.51543 81.6911 11.2596Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M85.7692 13.4495C85.7692 14.9417 87.022 16.3177 89.0776 16.3177C90.5065 16.3177 91.4269 15.6588 91.916 14.9029C91.916 15.2712 91.9554 15.7944 92.014 16.0464H94.4023C94.3439 15.7169 94.2851 15.0386 94.2851 14.5348V9.84482C94.2851 7.92622 93.1495 6.22083 90.0955 6.22083C87.5115 6.22083 86.1216 7.86807 85.965 9.36033L88.2747 9.84482C88.3533 9.0115 88.9798 8.29445 90.1149 8.29445C91.2115 8.29445 91.74 8.85646 91.74 9.53475C91.74 9.86421 91.5638 10.1355 91.0156 10.2131L88.647 10.5619C87.0418 10.7945 85.7692 11.744 85.7692 13.4495ZM89.6258 14.3991C88.784 14.3991 88.3727 13.8565 88.3727 13.2945C88.3727 12.558 88.9012 12.1898 89.5671 12.0929L91.74 11.7634V12.1898C91.74 13.8759 90.722 14.3991 89.6258 14.3991Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M96.087 13.3913C96.2042 14.4766 97.2028 16.3371 100.1 16.3371C102.626 16.3371 103.839 14.7479 103.839 13.1976C103.839 11.8022 102.88 10.6588 100.981 10.2712L99.6105 9.98049C99.082 9.88359 98.7299 9.5929 98.7299 9.12777C98.7299 8.58512 99.2778 8.17818 99.963 8.17818C101.06 8.17818 101.471 8.89521 101.549 9.45725L103.722 8.97275C103.604 7.94561 102.684 6.22083 99.9436 6.22083C97.8683 6.22083 96.3416 7.63555 96.3416 9.34094C96.3416 10.6781 97.183 11.7828 99.043 12.1898L100.316 12.4805C101.06 12.6355 101.353 12.9844 101.353 13.4107C101.353 13.9146 100.942 14.3603 100.081 14.3603C98.9451 14.3603 98.3776 13.6626 98.3188 12.9068L96.087 13.3913Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M107.794 10.1937C107.852 9.32158 108.596 8.31381 109.947 8.31381C111.435 8.31381 112.062 9.24406 112.101 10.1937H107.794ZM112.355 12.6743C112.042 13.527 111.376 14.1278 110.163 14.1278C108.87 14.1278 107.794 13.2169 107.735 11.9573H114.626C114.626 11.9184 114.665 11.5309 114.665 11.1626C114.665 8.10064 112.884 6.22083 109.908 6.22083C107.441 6.22083 105.17 8.19753 105.17 11.2402C105.17 14.4572 107.5 16.3371 110.143 16.3371C112.512 16.3371 114.039 14.9611 114.528 13.3138L112.355 12.6743Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_4671_51136"
|
||||
x1="11.4954"
|
||||
y1="11.1486"
|
||||
x2="19.3439"
|
||||
y2="14.4777"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#249361" />
|
||||
<stop offset="1" stopColor="#3ECF8E" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_4671_51136"
|
||||
x1="8.00382"
|
||||
y1="6.42177"
|
||||
x2="11.5325"
|
||||
y2="13.1398"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_4671_51136">
|
||||
<rect
|
||||
width="113.85"
|
||||
height="21.8943"
|
||||
fill="currentColor"
|
||||
transform="translate(0.922119 0.456161)"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_4671_51136">
|
||||
<rect
|
||||
width="21.3592"
|
||||
height="21.8943"
|
||||
fill="currentColor"
|
||||
transform="translate(0.919006 0.497101)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Laptop, Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const ThemeSwitcher = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
// useEffect only runs on the client, so now we can safely show the UI
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ICON_SIZE = 16;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size={"sm"}>
|
||||
{theme === "light" ? (
|
||||
<Sun
|
||||
key="light"
|
||||
size={ICON_SIZE}
|
||||
className={"text-muted-foreground"}
|
||||
/>
|
||||
) : theme === "dark" ? (
|
||||
<Moon
|
||||
key="dark"
|
||||
size={ICON_SIZE}
|
||||
className={"text-muted-foreground"}
|
||||
/>
|
||||
) : (
|
||||
<Laptop
|
||||
key="system"
|
||||
size={ICON_SIZE}
|
||||
className={"text-muted-foreground"}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-content" align="start">
|
||||
<DropdownMenuRadioGroup
|
||||
value={theme}
|
||||
onValueChange={(e) => setTheme(e)}
|
||||
>
|
||||
<DropdownMenuRadioItem className="flex gap-2" value="light">
|
||||
<Sun size={ICON_SIZE} className="text-muted-foreground" />{" "}
|
||||
<span>Light</span>
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem className="flex gap-2" value="dark">
|
||||
<Moon size={ICON_SIZE} className="text-muted-foreground" />{" "}
|
||||
<span>Dark</span>
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem className="flex gap-2" value="system">
|
||||
<Laptop size={ICON_SIZE} className="text-muted-foreground" />{" "}
|
||||
<span>System</span>
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export { ThemeSwitcher };
|
||||
@@ -1,61 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
const CopyIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CheckIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export function CodeBlock({ code }: { code: string }) {
|
||||
const [icon, setIcon] = useState(CopyIcon);
|
||||
|
||||
const copy = async () => {
|
||||
await navigator?.clipboard?.writeText(code);
|
||||
setIcon(CheckIcon);
|
||||
setTimeout(() => setIcon(CopyIcon), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<pre className="bg-muted rounded-md p-6 my-6 relative">
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={copy}
|
||||
variant={"outline"}
|
||||
className="absolute right-2 top-2"
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
<code className="text-xs p-3">{code}</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { TutorialStep } from "./tutorial-step";
|
||||
|
||||
export default function ConnectSupabaseSteps() {
|
||||
return (
|
||||
<ol className="flex flex-col gap-6">
|
||||
<TutorialStep title="Create Supabase project">
|
||||
<p>
|
||||
Head over to{" "}
|
||||
<a
|
||||
href="https://app.supabase.com/project/_/settings/api"
|
||||
target="_blank"
|
||||
className="font-bold hover:underline text-foreground/80"
|
||||
rel="noreferrer"
|
||||
>
|
||||
database.new
|
||||
</a>{" "}
|
||||
and create a new Supabase project.
|
||||
</p>
|
||||
</TutorialStep>
|
||||
|
||||
<TutorialStep title="Declare environment variables">
|
||||
<p>
|
||||
Rename the{" "}
|
||||
<span className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-medium text-secondary-foreground border">
|
||||
.env.example
|
||||
</span>{" "}
|
||||
file in your Next.js app to{" "}
|
||||
<span className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-medium text-secondary-foreground border">
|
||||
.env.local
|
||||
</span>{" "}
|
||||
and populate with values from{" "}
|
||||
<a
|
||||
href="https://app.supabase.com/project/_/settings/api"
|
||||
target="_blank"
|
||||
className="font-bold hover:underline text-foreground/80"
|
||||
rel="noreferrer"
|
||||
>
|
||||
your Supabase project's API Settings
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</TutorialStep>
|
||||
|
||||
<TutorialStep title="Restart your Next.js development server">
|
||||
<p>
|
||||
You may need to quit your Next.js development server and run{" "}
|
||||
<span className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-medium text-secondary-foreground border">
|
||||
npm run dev
|
||||
</span>{" "}
|
||||
again to load the new environment variables.
|
||||
</p>
|
||||
</TutorialStep>
|
||||
|
||||
<TutorialStep title="Refresh the page">
|
||||
<p>
|
||||
You may need to refresh the page for Next.js to load the new
|
||||
environment variables.
|
||||
</p>
|
||||
</TutorialStep>
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import { TutorialStep } from "./tutorial-step";
|
||||
import { CodeBlock } from "./code-block";
|
||||
|
||||
const create = `create table notes (
|
||||
id bigserial primary key,
|
||||
title text
|
||||
);
|
||||
|
||||
insert into notes(title)
|
||||
values
|
||||
('Today I created a Supabase project.'),
|
||||
('I added some data and queried it from Next.js.'),
|
||||
('It was awesome!');
|
||||
`.trim();
|
||||
|
||||
const server = `import { createClient } from '@/utils/supabase/server'
|
||||
|
||||
export default async function Page() {
|
||||
const supabase = await createClient()
|
||||
const { data: notes } = await supabase.from('notes').select()
|
||||
|
||||
return <pre>{JSON.stringify(notes, null, 2)}</pre>
|
||||
}
|
||||
`.trim();
|
||||
|
||||
const client = `'use client'
|
||||
|
||||
import { createClient } from '@/utils/supabase/client'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export default function Page() {
|
||||
const [notes, setNotes] = useState<any[] | null>(null)
|
||||
const supabase = createClient()
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async () => {
|
||||
const { data } = await supabase.from('notes').select()
|
||||
setNotes(data)
|
||||
}
|
||||
getData()
|
||||
}, [])
|
||||
|
||||
return <pre>{JSON.stringify(notes, null, 2)}</pre>
|
||||
}
|
||||
`.trim();
|
||||
|
||||
export default function FetchDataSteps() {
|
||||
return (
|
||||
<ol className="flex flex-col gap-6">
|
||||
<TutorialStep title="Create some tables and insert some data">
|
||||
<p>
|
||||
Head over to the{" "}
|
||||
<a
|
||||
href="https://supabase.com/dashboard/project/_/editor"
|
||||
className="font-bold hover:underline text-foreground/80"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Table Editor
|
||||
</a>{" "}
|
||||
for your Supabase project to create a table and insert some example
|
||||
data. If you're stuck for creativity, you can copy and paste the
|
||||
following into the{" "}
|
||||
<a
|
||||
href="https://supabase.com/dashboard/project/_/sql/new"
|
||||
className="font-bold hover:underline text-foreground/80"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
SQL Editor
|
||||
</a>{" "}
|
||||
and click RUN!
|
||||
</p>
|
||||
<CodeBlock code={create} />
|
||||
</TutorialStep>
|
||||
|
||||
<TutorialStep title="Query Supabase data from Next.js">
|
||||
<p>
|
||||
To create a Supabase client and query data from an Async Server
|
||||
Component, create a new page.tsx file at{" "}
|
||||
<span className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-medium text-secondary-foreground border">
|
||||
/app/notes/page.tsx
|
||||
</span>{" "}
|
||||
and add the following.
|
||||
</p>
|
||||
<CodeBlock code={server} />
|
||||
<p>Alternatively, you can use a Client Component.</p>
|
||||
<CodeBlock code={client} />
|
||||
</TutorialStep>
|
||||
|
||||
<TutorialStep title="Build in a weekend and scale to millions!">
|
||||
<p>You're ready to launch your product to the world! 🚀</p>
|
||||
</TutorialStep>
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { TutorialStep } from "./tutorial-step";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
export default function SignUpUserSteps() {
|
||||
return (
|
||||
<ol className="flex flex-col gap-6">
|
||||
{process.env.VERCEL_ENV === "preview" ||
|
||||
process.env.VERCEL_ENV === "production" ? (
|
||||
<TutorialStep title="Set up redirect urls">
|
||||
<p>It looks like this App is hosted on Vercel.</p>
|
||||
<p className="mt-4">
|
||||
This particular deployment is
|
||||
<span className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-medium text-secondary-foreground border">
|
||||
"{process.env.VERCEL_ENV}"
|
||||
</span>{" "}
|
||||
on
|
||||
<span className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-medium text-secondary-foreground border">
|
||||
https://{process.env.VERCEL_URL}
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
You will need to{" "}
|
||||
<Link
|
||||
className="text-primary hover:text-foreground"
|
||||
href={
|
||||
"https://supabase.com/dashboard/project/_/auth/url-configuration"
|
||||
}
|
||||
>
|
||||
update your Supabase project
|
||||
</Link>{" "}
|
||||
with redirect URLs based on your Vercel deployment URLs.
|
||||
</p>
|
||||
<ul className="mt-4">
|
||||
<li>
|
||||
-{" "}
|
||||
<span className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-medium text-secondary-foreground border">
|
||||
http://localhost:3000/**
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
-{" "}
|
||||
<span className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-medium text-secondary-foreground border">
|
||||
{`https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}/**`}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
-{" "}
|
||||
<span className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-medium text-secondary-foreground border">
|
||||
{`https://${process.env.VERCEL_PROJECT_PRODUCTION_URL?.replace(".vercel.app", "")}-*-[vercel-team-url].vercel.app/**`}
|
||||
</span>{" "}
|
||||
(Vercel Team URL can be found in{" "}
|
||||
<Link
|
||||
className="text-primary hover:text-foreground"
|
||||
href="https://vercel.com/docs/accounts/create-a-team#find-your-team-id"
|
||||
target="_blank"
|
||||
>
|
||||
Vercel Team settings
|
||||
</Link>
|
||||
)
|
||||
</li>
|
||||
</ul>
|
||||
<Link
|
||||
href="https://supabase.com/docs/guides/auth/redirect-urls#vercel-preview-urls"
|
||||
target="_blank"
|
||||
className="text-primary/50 hover:text-primary flex items-center text-sm gap-1 mt-4"
|
||||
>
|
||||
Redirect URLs Docs <ArrowUpRight size={14} />
|
||||
</Link>
|
||||
</TutorialStep>
|
||||
) : null}
|
||||
<TutorialStep title="Sign up your first user">
|
||||
<p>
|
||||
Head over to the{" "}
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="font-bold hover:underline text-foreground/80"
|
||||
>
|
||||
Sign up
|
||||
</Link>{" "}
|
||||
page and sign up your first user. It's okay if this is just you for
|
||||
now. Your awesome idea will have plenty of users later!
|
||||
</p>
|
||||
</TutorialStep>
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
|
||||
export function TutorialStep({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<li className="relative">
|
||||
<Checkbox
|
||||
id={title}
|
||||
name={title}
|
||||
className={`absolute top-[3px] mr-2 peer`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={title}
|
||||
className={`relative text-base text-foreground peer-checked:line-through font-medium`}
|
||||
>
|
||||
<span className="ml-8">{title}</span>
|
||||
<div
|
||||
className={`ml-8 text-sm peer-checked:line-through font-normal text-muted-foreground`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export function TypographyInlineCode() {
|
||||
return (
|
||||
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">
|
||||
@radix-ui/react-alert-dialog
|
||||
</code>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/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<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
@@ -1,56 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-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<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -1,30 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
||||
@@ -1,200 +0,0 @@
|
||||
"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 "@/lib/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<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
@@ -1,26 +0,0 @@
|
||||
"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 "@/lib/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<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
Reference in New Issue
Block a user