mirror of
https://github.com/documenso/documenso.git
synced 2025-11-11 04:52:41 +10:00
Compare commits
2 Commits
v2.0.1
...
chore/stat
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f532208cb | |||
| fa25f6d24e |
@ -19,6 +19,7 @@
|
|||||||
"@documenso/trpc": "*",
|
"@documenso/trpc": "*",
|
||||||
"@documenso/ui": "*",
|
"@documenso/ui": "*",
|
||||||
"@hookform/resolvers": "^3.1.0",
|
"@hookform/resolvers": "^3.1.0",
|
||||||
|
"@openstatus/react": "^0.0.3",
|
||||||
"contentlayer": "^0.3.4",
|
"contentlayer": "^0.3.4",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.279.0",
|
"lucide-react": "^0.279.0",
|
||||||
|
|||||||
@ -1,78 +1,14 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
|
|
||||||
import launchWeekTwoImage from '@documenso/assets/images/background-lw-2.png';
|
|
||||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
|
|
||||||
import { Footer } from '~/components/(marketing)/footer';
|
import { Footer } from '~/components/(marketing)/footer';
|
||||||
import { Header } from '~/components/(marketing)/header';
|
import { LayoutHeader } from '~/components/(marketing)/layout-header';
|
||||||
|
|
||||||
export type MarketingLayoutProps = {
|
export type MarketingLayoutProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
||||||
const [scrollY, setScrollY] = useState(0);
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const { getFlag } = useFeatureFlags();
|
|
||||||
|
|
||||||
const showProfilesAnnouncementBar = getFlag('marketing_profiles_announcement_bar');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onScroll = () => {
|
|
||||||
setScrollY(window.scrollY);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('scroll', onScroll);
|
|
||||||
|
|
||||||
return () => window.removeEventListener('scroll', onScroll);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="relative flex min-h-[100vh] max-w-[100vw] flex-col overflow-y-auto overflow-x-hidden pt-20 md:pt-28">
|
||||||
className={cn('relative flex min-h-[100vh] max-w-[100vw] flex-col pt-20 md:pt-28', {
|
<LayoutHeader />
|
||||||
'overflow-y-auto overflow-x-hidden': pathname !== '/singleplayer',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn('fixed left-0 top-0 z-50 w-full bg-transparent', {
|
|
||||||
'bg-background/50 backdrop-blur-md': scrollY > 5,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{showProfilesAnnouncementBar && (
|
|
||||||
<div className="relative inline-flex w-full items-center justify-center overflow-hidden px-4 py-2.5">
|
|
||||||
<div className="absolute inset-0 -z-[1]">
|
|
||||||
<Image
|
|
||||||
src={launchWeekTwoImage}
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
alt="Launch Week 2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-background text-center text-sm text-white">
|
|
||||||
Claim your documenso public profile username now!{' '}
|
|
||||||
<span className="hidden font-semibold md:inline">documenso.com/u/yourname</span>
|
|
||||||
<div className="mt-1.5 block md:ml-4 md:mt-0 md:inline-block">
|
|
||||||
<a
|
|
||||||
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=marketing-announcement-bar`}
|
|
||||||
className="bg-background text-foreground rounded-md px-2.5 py-1 text-xs font-medium duration-300"
|
|
||||||
>
|
|
||||||
Claim Now
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative max-w-screen-xl flex-1 px-4 sm:mx-auto lg:px-8">{children}</div>
|
<div className="relative max-w-screen-xl flex-1 px-4 sm:mx-auto lg:px-8">{children}</div>
|
||||||
|
|
||||||
|
|||||||
@ -184,83 +184,85 @@ export const SinglePlayerClient = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 sm:mt-12">
|
<div className="overflow-x-auto overflow-y-hidden">
|
||||||
<div className="text-center">
|
<div className="mt-6 sm:mt-12">
|
||||||
<h1 className="text-3xl font-bold lg:text-5xl">Single Player Mode</h1>
|
<div className="text-center">
|
||||||
|
<h1 className="text-3xl font-bold lg:text-5xl">Single Player Mode</h1>
|
||||||
|
|
||||||
<p className="text-foreground mx-auto mt-4 max-w-[50ch] text-lg leading-normal">
|
<p className="text-foreground mx-auto mt-4 max-w-[50ch] text-lg leading-normal">
|
||||||
Create a{' '}
|
Create a{' '}
|
||||||
<Link
|
<Link
|
||||||
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=singleplayer`}
|
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=singleplayer`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="hover:text-foreground/80 font-semibold transition-colors"
|
className="hover:text-foreground/80 font-semibold transition-colors"
|
||||||
>
|
>
|
||||||
free account
|
free account
|
||||||
</Link>{' '}
|
</Link>{' '}
|
||||||
or view our{' '}
|
or view our{' '}
|
||||||
<Link
|
<Link
|
||||||
href={'/pricing'}
|
href={'/pricing'}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="hover:text-foreground/80 font-semibold transition-colors"
|
className="hover:text-foreground/80 font-semibold transition-colors"
|
||||||
>
|
>
|
||||||
early adopter plan
|
early adopter plan
|
||||||
</Link>{' '}
|
</Link>{' '}
|
||||||
for exclusive features, including the ability to collaborate with multiple signers.
|
for exclusive features, including the ability to collaborate with multiple signers.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-12 grid w-full grid-cols-12 gap-8">
|
|
||||||
<div className="col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7">
|
|
||||||
{uploadedFile ? (
|
|
||||||
<Card gradient>
|
|
||||||
<CardContent className="p-2">
|
|
||||||
<LazyPDFViewer
|
|
||||||
documentData={{
|
|
||||||
id: '',
|
|
||||||
data: uploadedFile.fileBase64,
|
|
||||||
initialData: uploadedFile.fileBase64,
|
|
||||||
type: DocumentDataType.BYTES_64,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<DocumentDropzone className="h-[80vh] max-h-[60rem]" onDrop={onFileDrop} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
<div className="mt-12 grid w-full grid-cols-12 gap-8">
|
||||||
<DocumentFlowFormContainer
|
<div className="col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7">
|
||||||
className="top-24 lg:h-[calc(100vh-7rem)]"
|
{uploadedFile ? (
|
||||||
onSubmit={(e) => e.preventDefault()}
|
<Card gradient>
|
||||||
>
|
<CardContent className="p-2">
|
||||||
<Stepper
|
<LazyPDFViewer
|
||||||
currentStep={currentDocumentFlow.stepIndex}
|
documentData={{
|
||||||
setCurrentStep={(step) => setStep(SinglePlayerModeSteps[step - 1])}
|
id: '',
|
||||||
|
data: uploadedFile.fileBase64,
|
||||||
|
initialData: uploadedFile.fileBase64,
|
||||||
|
type: DocumentDataType.BYTES_64,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<DocumentDropzone className="h-[80vh] max-h-[60rem]" onDrop={onFileDrop} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
||||||
|
<DocumentFlowFormContainer
|
||||||
|
className="top-24 lg:h-[calc(100vh-7rem)]"
|
||||||
|
onSubmit={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
{/* Add fields to PDF page. */}
|
<Stepper
|
||||||
<fieldset disabled={!uploadedFile} className="flex h-full flex-col">
|
currentStep={currentDocumentFlow.stepIndex}
|
||||||
<AddFieldsFormPartial
|
setCurrentStep={(step) => setStep(SinglePlayerModeSteps[step - 1])}
|
||||||
documentFlow={documentFlow.fields}
|
>
|
||||||
hideRecipients={true}
|
{/* Add fields to PDF page. */}
|
||||||
recipients={uploadedFile ? [placeholderRecipient] : []}
|
<fieldset disabled={!uploadedFile} className="flex h-full flex-col">
|
||||||
|
<AddFieldsFormPartial
|
||||||
|
documentFlow={documentFlow.fields}
|
||||||
|
hideRecipients={true}
|
||||||
|
recipients={uploadedFile ? [placeholderRecipient] : []}
|
||||||
|
fields={fields}
|
||||||
|
onSubmit={onFieldsSubmit}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{/* Enter user details and signature. */}
|
||||||
|
|
||||||
|
<AddSignatureFormPartial
|
||||||
|
documentFlow={documentFlow.sign}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onSubmit={onFieldsSubmit}
|
onSubmit={onSignSubmit}
|
||||||
|
requireName={Boolean(fields.find((field) => field.type === 'NAME'))}
|
||||||
|
requireCustomText={Boolean(fields.find((field) => field.type === 'TEXT'))}
|
||||||
|
requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</Stepper>
|
||||||
|
</DocumentFlowFormContainer>
|
||||||
{/* Enter user details and signature. */}
|
</div>
|
||||||
|
|
||||||
<AddSignatureFormPartial
|
|
||||||
documentFlow={documentFlow.sign}
|
|
||||||
fields={fields}
|
|
||||||
onSubmit={onSignSubmit}
|
|
||||||
requireName={Boolean(fields.find((field) => field.type === 'NAME'))}
|
|
||||||
requireCustomText={Boolean(fields.find((field) => field.type === 'TEXT'))}
|
|
||||||
requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))}
|
|
||||||
/>
|
|
||||||
</Stepper>
|
|
||||||
</DocumentFlowFormContainer>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
@ -13,6 +11,8 @@ import LogoImage from '@documenso/assets/logo.png';
|
|||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
||||||
|
|
||||||
|
import { StatusWidget } from './status-widget';
|
||||||
|
|
||||||
export type FooterProps = HTMLAttributes<HTMLDivElement>;
|
export type FooterProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
const SOCIAL_LINKS = [
|
const SOCIAL_LINKS = [
|
||||||
@ -62,6 +62,10 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<StatusWidget />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid w-full max-w-sm grid-cols-2 gap-x-4 gap-y-2 md:w-auto md:gap-x-8">
|
<div className="grid w-full max-w-sm grid-cols-2 gap-x-4 gap-y-2 md:w-auto md:gap-x-8">
|
||||||
|
|||||||
65
apps/marketing/src/components/(marketing)/layout-header.tsx
Normal file
65
apps/marketing/src/components/(marketing)/layout-header.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
import launchWeekTwoImage from '@documenso/assets/images/background-lw-2.png';
|
||||||
|
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
|
import { Header } from '~/components/(marketing)/header';
|
||||||
|
|
||||||
|
export function LayoutHeader() {
|
||||||
|
const [scrollY, setScrollY] = useState(0);
|
||||||
|
|
||||||
|
const { getFlag } = useFeatureFlags();
|
||||||
|
|
||||||
|
const showProfilesAnnouncementBar = getFlag('marketing_profiles_announcement_bar');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onScroll = () => {
|
||||||
|
setScrollY(window.scrollY);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', onScroll);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('scroll', onScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('fixed left-0 top-0 z-50 w-full bg-transparent', {
|
||||||
|
'bg-background/50 backdrop-blur-md': scrollY > 5,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{showProfilesAnnouncementBar && (
|
||||||
|
<div className="relative inline-flex w-full items-center justify-center overflow-hidden px-4 py-2.5">
|
||||||
|
<div className="absolute inset-0 -z-[1]">
|
||||||
|
<Image
|
||||||
|
src={launchWeekTwoImage}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
alt="Launch Week 2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-background text-center text-sm text-white">
|
||||||
|
Claim your documenso public profile username now!{' '}
|
||||||
|
<span className="hidden font-semibold md:inline">documenso.com/u/yourname</span>
|
||||||
|
<div className="mt-1.5 block md:ml-4 md:mt-0 md:inline-block">
|
||||||
|
<a
|
||||||
|
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=marketing-announcement-bar`}
|
||||||
|
className="bg-background text-foreground rounded-md px-2.5 py-1 text-xs font-medium duration-300"
|
||||||
|
>
|
||||||
|
Claim Now
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
apps/marketing/src/components/(marketing)/status-widget.tsx
Normal file
73
apps/marketing/src/components/(marketing)/status-widget.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import type { Status } from '@openstatus/react';
|
||||||
|
import { getStatus } from '@openstatus/react';
|
||||||
|
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
|
const getStatusLevel = (level: Status) => {
|
||||||
|
return {
|
||||||
|
operational: {
|
||||||
|
label: 'Operational',
|
||||||
|
color: 'bg-green-500',
|
||||||
|
color2: 'bg-green-400',
|
||||||
|
},
|
||||||
|
degraded_performance: {
|
||||||
|
label: 'Degraded Performance',
|
||||||
|
color: 'bg-yellow-500',
|
||||||
|
color2: 'bg-yellow-400',
|
||||||
|
},
|
||||||
|
partial_outage: {
|
||||||
|
label: 'Partial Outage',
|
||||||
|
color: 'bg-yellow-500',
|
||||||
|
color2: 'bg-yellow-400',
|
||||||
|
},
|
||||||
|
major_outage: {
|
||||||
|
label: 'Major Outage',
|
||||||
|
color: 'bg-red-500',
|
||||||
|
color2: 'bg-red-400',
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
label: 'Unknown',
|
||||||
|
color: 'bg-gray-500',
|
||||||
|
color2: 'bg-gray-400',
|
||||||
|
},
|
||||||
|
incident: {
|
||||||
|
label: 'Incident',
|
||||||
|
color: 'bg-yellow-500',
|
||||||
|
color2: 'bg-yellow-400',
|
||||||
|
},
|
||||||
|
under_maintenance: {
|
||||||
|
label: 'Under Maintenance',
|
||||||
|
color: 'bg-gray-500',
|
||||||
|
color2: 'bg-gray-400',
|
||||||
|
},
|
||||||
|
}[level];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function StatusWidget() {
|
||||||
|
const { status } = await getStatus('documenso');
|
||||||
|
|
||||||
|
const level = getStatusLevel(status);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className="border-border inline-flex max-w-fit items-center justify-between gap-2 space-x-2 rounded-md border border-gray-200 px-3 py-1 text-sm"
|
||||||
|
href="https://status.documenso.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm">{level.label}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="relative ml-auto flex h-1.5 w-1.5">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'absolute inline-flex h-full w-full animate-ping rounded-full opacity-75',
|
||||||
|
level.color2,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className={cn('relative inline-flex h-1.5 w-1.5 rounded-full', level.color)} />
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
package-lock.json
generated
9
package-lock.json
generated
@ -42,6 +42,7 @@
|
|||||||
"@documenso/trpc": "*",
|
"@documenso/trpc": "*",
|
||||||
"@documenso/ui": "*",
|
"@documenso/ui": "*",
|
||||||
"@hookform/resolvers": "^3.1.0",
|
"@hookform/resolvers": "^3.1.0",
|
||||||
|
"@openstatus/react": "^0.0.3",
|
||||||
"contentlayer": "^0.3.4",
|
"contentlayer": "^0.3.4",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"lucide-react": "^0.279.0",
|
"lucide-react": "^0.279.0",
|
||||||
@ -4124,6 +4125,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
||||||
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
|
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@openstatus/react": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@openstatus/react/-/react-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-uDiegz7e3H67pG8lTT+op+6w5keTT7XpcENrREaqlWl5j53TYyO8nheOG1PeNw2/Qgd5KaGeRJJFn1crhTUSYw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@opentelemetry/api": {
|
"node_modules/@opentelemetry/api": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz",
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Monitor, MoonStar, Sun } from 'lucide-react';
|
import { Monitor, MoonStar, Sun } from 'lucide-react';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
|
|||||||
Reference in New Issue
Block a user