mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
More progress
This commit is contained in:
parent
a65942d4fc
commit
6ee2266c36
53 changed files with 481 additions and 822 deletions
|
|
@ -16,12 +16,12 @@ export default function DashboardLayout({
|
|||
return (
|
||||
<>
|
||||
<div className="shadow-md">
|
||||
<div className="mx-auto flex max-w-screen-xl items-center justify-between px-3 py-4">
|
||||
<div className="mx-auto flex max-w-screen-lg items-center justify-between px-4 py-4">
|
||||
<DashboardHeader />
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-[calc(100vh-72px)] bg-muted">
|
||||
<div className="mx-auto max-w-screen-xl px-3 pb-16 pt-6">
|
||||
<div className="mx-auto max-w-screen-lg px-4 pb-16 pt-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
'use client';
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import { dark } from '@clerk/themes';
|
||||
import { OrganizationProfile } from '@clerk/nextjs';
|
||||
|
||||
export const OrgProfile = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
|
||||
return (
|
||||
<OrganizationProfile
|
||||
appearance={{ baseTheme }}
|
||||
routing="path"
|
||||
path="/dashboard/org"
|
||||
afterLeaveOrganizationUrl="/onboarding/select-org"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { OrgProfile } from './OrgProfile';
|
||||
|
||||
export default function Page() {
|
||||
return <OrgProfile />;
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { OrganizationProfile } from '@clerk/nextjs';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { TitleBar } from '@/components/dashboard/TitleBar';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('OrganizationProfile');
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleBar
|
||||
title={t('title_bar')}
|
||||
description={t('title_bar_description')}
|
||||
/>
|
||||
<OrganizationProfile
|
||||
routing="path"
|
||||
path="/dashboard/organization-profile"
|
||||
afterLeaveOrganizationUrl="/onboarding/select-org"
|
||||
appearance={{ elements: { rootBox: 'w-full', cardBox: 'w-full flex' } }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,36 +2,10 @@ import { useTranslations } from 'next-intl';
|
|||
|
||||
import { TitleBar } from '@/components/dashboard/TitleBar';
|
||||
|
||||
const DashboardIndexPage = () => {
|
||||
export default function Page() {
|
||||
const t = useTranslations('DashboardIndex');
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleBar
|
||||
title={t('title_bar')}
|
||||
description={t('title_bar_description')}
|
||||
/>
|
||||
<div className="flex h-[600px] flex-col items-center justify-center rounded-md bg-card p-5">
|
||||
<div className="size-16 rounded-full bg-muted p-3 [&_svg]:stroke-muted-foreground [&_svg]:stroke-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3M12 12l8-4.5M12 12v9M12 12L4 7.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="mt-3 text-center">
|
||||
<div className="text-xl font-semibold">
|
||||
{t('message_state_title')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<TitleBar title={t('title_bar')} description={t('title_bar_description')} />
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardIndexPage;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
import { UserProfile } from '@clerk/nextjs';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { TitleBar } from '@/components/dashboard/TitleBar';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('UserProfile');
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleBar
|
||||
title={t('title_bar')}
|
||||
description={t('title_bar_description')}
|
||||
/>
|
||||
<UserProfile
|
||||
routing="path"
|
||||
path="/dashboard/user-profile"
|
||||
appearance={{ elements: { rootBox: 'w-full', cardBox: 'w-full flex' } }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
'use client';
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import { dark } from '@clerk/themes';
|
||||
import { UserProfile as ClerkUserProfile } from '@clerk/nextjs';
|
||||
|
||||
export const UserProfile = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
|
||||
return (
|
||||
<ClerkUserProfile
|
||||
appearance={{ baseTheme }}
|
||||
routing="path"
|
||||
path="/dashboard/user"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { UserProfile } from './UserProfile';
|
||||
|
||||
export default function Page() {
|
||||
return <UserProfile />;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { redirect } from 'next/navigation';
|
||||
import { auth as clerkAuth } from '@clerk/nextjs/server';
|
||||
|
||||
export default async function CenteredLayout({
|
||||
export default async function AuthenticatedLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { getLocale } from 'next-intl/server';
|
|||
import { enUS, frFR } from '@clerk/localizations';
|
||||
import { ClerkProvider } from '@clerk/nextjs';
|
||||
|
||||
import { ThemeProvider } from '@/components/ThemeProvider';
|
||||
import { ThemeProvider } from '@/components/layout/ThemeProvider';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
icons: [
|
||||
|
|
|
|||
17
src/app/onboarding/create-org/CreateOrg.tsx
Normal file
17
src/app/onboarding/create-org/CreateOrg.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
'use client';
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import { CreateOrganization } from '@clerk/nextjs';
|
||||
import { dark } from '@clerk/themes';
|
||||
|
||||
export const CreateOrg = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
|
||||
return (
|
||||
<CreateOrganization
|
||||
appearance={{ baseTheme }}
|
||||
afterCreateOrganizationUrl="/dashboard"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import { redirect } from 'next/navigation';
|
||||
import { CreateOrganization } from '@clerk/nextjs';
|
||||
import { auth as clerkAuth } from '@clerk/nextjs/server';
|
||||
import { getLocale, getTranslations } from 'next-intl/server';
|
||||
|
||||
import { CreateOrg } from './CreateOrg';
|
||||
|
||||
export async function generateMetadata() {
|
||||
const locale = await getLocale();
|
||||
const t = await getTranslations({ locale, namespace: 'Dashboard' });
|
||||
|
|
@ -12,18 +11,5 @@ export async function generateMetadata() {
|
|||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function Page() {
|
||||
const auth = await clerkAuth();
|
||||
|
||||
if (!auth.userId) {
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<CreateOrganization
|
||||
// afterSelectOrganizationUrl="/dashboard"
|
||||
afterCreateOrganizationUrl="/dashboard"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return <CreateOrg />;
|
||||
}
|
||||
|
|
|
|||
20
src/app/onboarding/layout.tsx
Normal file
20
src/app/onboarding/layout.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { redirect } from 'next/navigation';
|
||||
import { auth as clerkAuth } from '@clerk/nextjs/server';
|
||||
|
||||
export default async function OnboardingLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const auth = await clerkAuth();
|
||||
|
||||
if (!auth.userId) {
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
src/app/onboarding/select-org/SelectOrg.tsx
Normal file
18
src/app/onboarding/select-org/SelectOrg.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
'use client';
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import { dark } from '@clerk/themes';
|
||||
import { OrganizationList } from '@clerk/nextjs';
|
||||
|
||||
export const SelectOrg = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
|
||||
return (
|
||||
<OrganizationList
|
||||
appearance={{ baseTheme }}
|
||||
afterSelectOrganizationUrl="/dashboard"
|
||||
afterCreateOrganizationUrl="/dashboard"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import { redirect } from 'next/navigation';
|
||||
import { OrganizationList } from '@clerk/nextjs';
|
||||
import { auth as clerkAuth } from '@clerk/nextjs/server';
|
||||
import { getLocale, getTranslations } from 'next-intl/server';
|
||||
|
||||
import { SelectOrg } from './SelectOrg';
|
||||
|
||||
export async function generateMetadata() {
|
||||
const locale = await getLocale();
|
||||
const t = await getTranslations({ locale, namespace: 'Dashboard' });
|
||||
|
|
@ -12,18 +11,5 @@ export async function generateMetadata() {
|
|||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function Page() {
|
||||
const auth = await clerkAuth();
|
||||
|
||||
if (!auth.userId) {
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<OrganizationList
|
||||
afterSelectOrganizationUrl="/dashboard"
|
||||
afterCreateOrganizationUrl="/dashboard"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return <SelectOrg />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,20 @@
|
|||
import { redirect } from 'next/navigation';
|
||||
import { auth as clerkAuth } from '@clerk/nextjs/server';
|
||||
import { getLocale, getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
|
||||
import { Features } from '@/components/templates/Features';
|
||||
import { Footer } from '@/components/templates/Footer';
|
||||
import { Hero } from '@/components/templates/Hero';
|
||||
import { Navbar } from '@/components/templates/Navbar';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { Navbar, Footer } from '@/components/layout';
|
||||
import { Hero, Features } from '@/components/landing';
|
||||
|
||||
export async function generateMetadata() {
|
||||
const locale = await getLocale();
|
||||
const t = await getTranslations({ locale, namespace: 'Index' });
|
||||
|
||||
return {
|
||||
title: t('meta_title'),
|
||||
description: t('meta_description'),
|
||||
};
|
||||
return { title: t('meta_title'), description: t('meta_description') };
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
const auth = await clerkAuth();
|
||||
const { userId } = await clerkAuth();
|
||||
|
||||
if (auth.userId) {
|
||||
if (userId) {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
|
||||
|
|
|
|||
22
src/app/sign-in/[[...sign-in]]/SignIn.tsx
Normal file
22
src/app/sign-in/[[...sign-in]]/SignIn.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { dark } from '@clerk/themes';
|
||||
import { SignIn as ClerkSignIn } from '@clerk/nextjs';
|
||||
|
||||
import { Logo } from '@/components/layout';
|
||||
|
||||
export const SignIn = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 h-screen w-full items-center justify-center">
|
||||
<Link href="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
<ClerkSignIn appearance={{ baseTheme }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { SignIn } from '@clerk/nextjs';
|
||||
import { getLocale, getTranslations } from 'next-intl/server';
|
||||
|
||||
import { SignIn } from './SignIn';
|
||||
|
||||
export async function generateMetadata() {
|
||||
const locale = await getLocale();
|
||||
const t = await getTranslations({ locale, namespace: 'SignIn' });
|
||||
|
|
@ -8,9 +9,5 @@ export async function generateMetadata() {
|
|||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
<SignIn forceRedirectUrl="/dashboard" />
|
||||
</div>
|
||||
);
|
||||
return <SignIn />;
|
||||
}
|
||||
|
|
|
|||
22
src/app/sign-up/[[...sign-up]]/SignUp.tsx
Normal file
22
src/app/sign-up/[[...sign-up]]/SignUp.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { dark } from '@clerk/themes';
|
||||
import { SignUp as ClerkSignUp } from '@clerk/nextjs';
|
||||
|
||||
import { Logo } from '@/components/layout';
|
||||
|
||||
export const SignUp = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 h-screen w-full items-center justify-center">
|
||||
<Link href="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
<ClerkSignUp appearance={{ baseTheme }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { SignUp } from '@clerk/nextjs';
|
||||
import { getLocale, getTranslations } from 'next-intl/server';
|
||||
|
||||
import { SignUp } from './SignUp';
|
||||
|
||||
export async function generateMetadata() {
|
||||
const locale = await getLocale();
|
||||
const t = await getTranslations({ locale, namespace: 'SignUp' });
|
||||
|
|
@ -8,9 +9,5 @@ export async function generateMetadata() {
|
|||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
<SignUp />
|
||||
</div>
|
||||
);
|
||||
return <SignUp />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type ActiveLinkProps = {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ActiveLink = ({ href, children }: ActiveLinkProps) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={cn(
|
||||
'px-3 py-2',
|
||||
pathname.endsWith(href) &&
|
||||
'rounded-md bg-primary text-primary-foreground',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { ToggleMenuButton } from './ToggleMenuButton';
|
||||
|
||||
describe('ToggleMenuButton', () => {
|
||||
describe('onClick props', () => {
|
||||
it('should call the callback when the user click on the button', async () => {
|
||||
const handler = vi.fn();
|
||||
|
||||
render(<ToggleMenuButton onClick={handler} />);
|
||||
const button = screen.getByRole('button');
|
||||
await userEvent.click(button);
|
||||
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import { type ForwardedRef, forwardRef } from 'react';
|
||||
|
||||
import { Button } from '@/components/ui';
|
||||
|
||||
type ToggleMenuButtonProps = {
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const ToggleMenuButtonInternal = (
|
||||
props: ToggleMenuButtonProps,
|
||||
ref?: ForwardedRef<HTMLButtonElement>,
|
||||
) => (
|
||||
<Button
|
||||
className="p-2 focus-visible:ring-offset-0"
|
||||
variant="ghost"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<svg
|
||||
className="size-6 stroke-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const ToggleMenuButton = forwardRef(ToggleMenuButtonInternal);
|
||||
|
||||
export { ToggleMenuButton };
|
||||
|
|
@ -1,111 +1,45 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
OrganizationSwitcher,
|
||||
UserButton,
|
||||
useOrganization,
|
||||
} from '@clerk/nextjs';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { OrganizationSwitcher, UserButton } from '@clerk/nextjs';
|
||||
import { dark } from '@clerk/themes';
|
||||
|
||||
import { ActiveLink } from '@/components/ActiveLink';
|
||||
import { LocaleSwitcher } from '@/components/LocaleSwitcher';
|
||||
import { ThemeSwitcher } from '@/components/ThemeSwitcher';
|
||||
import { ToggleMenuButton } from '@/components/ToggleMenuButton';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Separator,
|
||||
} from '@/components/ui';
|
||||
import { Logo } from '@/components/templates/Logo';
|
||||
import { LocaleSwitcher } from '@/components/layout/LocaleSwitcher';
|
||||
import { ThemeSwitcher } from '@/components/layout/ThemeSwitcher';
|
||||
import { Logo } from '@/components/layout/Logo';
|
||||
|
||||
export const DashboardHeader = () => {
|
||||
const t = useTranslations('DashboardLayout');
|
||||
const { organization } = useOrganization();
|
||||
|
||||
const menu = useMemo(() => {
|
||||
const menu = [{ href: '/dashboard', label: t('home') }];
|
||||
|
||||
return organization
|
||||
? [
|
||||
...menu,
|
||||
{
|
||||
href: '/dashboard/organization-profile/organization-members',
|
||||
label: t('members'),
|
||||
},
|
||||
{ href: '/dashboard/organization-profile', label: t('settings') },
|
||||
]
|
||||
: menu;
|
||||
}, [organization, t]);
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<Link href="/dashboard" className="max-sm:hidden">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/dashboard">
|
||||
<Logo />
|
||||
</Link>
|
||||
<svg
|
||||
className="size-8 stroke-muted-foreground max-sm:hidden"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" />
|
||||
<path d="M17 5 7 19" />
|
||||
</svg>
|
||||
<OrganizationSwitcher
|
||||
appearance={{ baseTheme }}
|
||||
organizationProfileMode="navigation"
|
||||
organizationProfileUrl="/dashboard/organization-profile"
|
||||
organizationProfileUrl="/dashboard/org"
|
||||
afterCreateOrganizationUrl="/dashboard"
|
||||
createOrganizationUrl="/onboarding/create-org"
|
||||
/>
|
||||
<nav className="ml-3 max-lg:hidden">
|
||||
<ul className="flex flex-row items-center gap-x-3 text-lg font-medium [&_a:hover]:opacity-100 [&_a]:opacity-75">
|
||||
{menu.map(({ href, label }) => (
|
||||
<li key={href}>
|
||||
<ActiveLink href={href}>{label}</ActiveLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div>
|
||||
<ul className="flex items-center gap-x-1.5 [&_li[data-fade]:hover]:opacity-100 [&_li[data-fade]]:opacity-60">
|
||||
<li data-fade>
|
||||
<div className="lg:hidden">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ToggleMenuButton />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{menu.map(({ href, label }) => (
|
||||
<DropdownMenuItem key={href} asChild>
|
||||
<Link href={href}>{label}</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</li>
|
||||
<ul className="flex items-center gap-2 [&_li[data-fade]:hover]:opacity-100 [&_li[data-fade]]:opacity-60">
|
||||
<li data-fade>
|
||||
<LocaleSwitcher />
|
||||
</li>
|
||||
<li data-fade>
|
||||
<ThemeSwitcher />
|
||||
</li>
|
||||
<li>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
</li>
|
||||
<li>
|
||||
<UserButton
|
||||
appearance={{ baseTheme }}
|
||||
userProfileMode="navigation"
|
||||
userProfileUrl="/dashboard/user-profile"
|
||||
appearance={{ elements: { rootBox: 'px-2 py-1.5' } }}
|
||||
userProfileUrl="/dashboard/user"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
type DashboardSectionProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const DashboardSection = ({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
}: DashboardSectionProps) => (
|
||||
<div className="rounded-md bg-card p-5">
|
||||
<div className="max-w-3xl">
|
||||
<div className="text-lg font-semibold">{title}</div>
|
||||
<div className="mb-4 text-sm font-medium text-muted-foreground">
|
||||
{description}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
export const TitleBar = (props: {
|
||||
type TitleBarProps = {
|
||||
title: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
}) => (
|
||||
<div className="mb-8">
|
||||
<div className="text-2xl font-semibold">{props.title}</div>
|
||||
};
|
||||
|
||||
{props.description && (
|
||||
export const TitleBar = ({ title, description }: TitleBarProps) => (
|
||||
<div className="mb-8">
|
||||
<div className="text-2xl font-semibold">{title}</div>
|
||||
{description && (
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
{props.description}
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
|
||||
import messages from '@/i18n/locales/en.json';
|
||||
|
||||
import { CenteredFooter } from './CenteredFooter';
|
||||
|
||||
describe('CenteredFooter', () => {
|
||||
describe('Render method', () => {
|
||||
it('should have copyright information', () => {
|
||||
render(
|
||||
<NextIntlClientProvider locale="en" messages={messages}>
|
||||
<CenteredFooter iconList={null} legalLinks={null}>
|
||||
Random children
|
||||
</CenteredFooter>
|
||||
</NextIntlClientProvider>,
|
||||
);
|
||||
|
||||
const copyright = screen.getByText(/© Copyright/);
|
||||
|
||||
expect(copyright).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { APP_NAME } from '@/lib/constants';
|
||||
|
||||
type CenteredFooterProps = {
|
||||
iconList: React.ReactNode;
|
||||
legalLinks: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CenteredFooter = ({
|
||||
iconList,
|
||||
legalLinks,
|
||||
children,
|
||||
}: CenteredFooterProps) => (
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{children && (
|
||||
<ul className="mt-4 flex gap-x-8 text-lg max-sm:flex-col [&_a:hover]:opacity-100 [&_a]:opacity-60">
|
||||
{children}
|
||||
</ul>
|
||||
)}
|
||||
<ul className="mt-4 flex flex-row gap-x-5 text-muted-foreground [&_svg:hover]:text-primary [&_svg:hover]:opacity-100 [&_svg]:size-5 [&_svg]:fill-current [&_svg]:opacity-60">
|
||||
{iconList}
|
||||
</ul>
|
||||
<div className="mt-6 flex w-full items-center justify-between gap-y-2 border-t pt-3 text-sm text-muted-foreground max-md:flex-col">
|
||||
<div>
|
||||
© Copyright {new Date().getFullYear()} {APP_NAME}.
|
||||
</div>
|
||||
<ul className="flex gap-x-4 font-medium [&_a:hover]:opacity-100 [&_a]:opacity-60">
|
||||
{legalLinks}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
type CenteredHeroProps = {
|
||||
title: React.ReactNode;
|
||||
description: string;
|
||||
buttons?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CenteredHero = ({
|
||||
title,
|
||||
description,
|
||||
buttons,
|
||||
}: CenteredHeroProps) => (
|
||||
<>
|
||||
<div className="mt-3 text-center text-5xl font-bold tracking-tight">
|
||||
{title}
|
||||
</div>
|
||||
<div className="mx-auto mt-5 max-w-screen-md text-center text-xl text-muted-foreground">
|
||||
{description}
|
||||
</div>
|
||||
{buttons && (
|
||||
<div className="mt-8 flex justify-center gap-x-5 gap-y-3 max-sm:flex-col">
|
||||
{buttons}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ToggleMenuButton } from '@/components/ToggleMenuButton';
|
||||
import { useMenu } from '@/hooks/useMenu';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type CenteredMenuProps = {
|
||||
logo: React.ReactNode;
|
||||
rightMenu: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CenteredMenu = ({
|
||||
logo,
|
||||
rightMenu,
|
||||
children,
|
||||
}: CenteredMenuProps) => {
|
||||
const { showMenu, handleToggleMenu } = useMenu();
|
||||
|
||||
const navClass = cn('max-lg:w-full max-lg:bg-secondary max-lg:p-5', {
|
||||
'max-lg:hidden': !showMenu,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-between">
|
||||
<Link href="/">{logo}</Link>
|
||||
<div className="lg:hidden [&_button:hover]:opacity-100 [&_button]:opacity-60">
|
||||
<ToggleMenuButton onClick={handleToggleMenu} />
|
||||
</div>
|
||||
{children && (
|
||||
<nav className={cn('rounded-t max-lg:mt-2', navClass)}>
|
||||
<ul className="flex gap-x-6 gap-y-1 text-lg font-medium max-lg:flex-col [&_a:hover]:opacity-100 [&_a]:opacity-60 max-lg:[&_a]:inline-block max-lg:[&_a]:w-full">
|
||||
{children}
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-b max-lg:border-t max-lg:border-border',
|
||||
navClass,
|
||||
)}
|
||||
>
|
||||
<ul className="flex flex-row items-center gap-x-1.5 text-lg font-medium [&_li[data-fade]:hover]:opacity-100 [&_li[data-fade]]:opacity-60">
|
||||
{rightMenu}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
export const FeatureCard = (props: {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<div className="rounded-xl border border-border bg-card p-5">
|
||||
<div className="size-12 rounded-lg bg-gradient-to-br from-indigo-400 via-purple-400 to-pink-400 p-2 [&_svg]:stroke-white [&_svg]:stroke-2">
|
||||
{props.icon}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-lg font-bold">{props.title}</div>
|
||||
|
||||
<div className="my-3 w-8 border-t border-purple-400" />
|
||||
|
||||
<div className="mt-2 text-muted-foreground">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
148
src/components/landing/Features.tsx
Normal file
148
src/components/landing/Features.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Section } from '@/components/layout';
|
||||
|
||||
export const Features = () => {
|
||||
const t = useTranslations('Features');
|
||||
|
||||
return (
|
||||
<Section className="bg-secondary">
|
||||
<div className="mx-auto mb-12 max-w-screen-md text-center">
|
||||
<div className="bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 bg-clip-text text-sm font-bold text-transparent">
|
||||
{t('section_subtitle')}
|
||||
</div>
|
||||
<div className="mt-1 text-3xl font-bold">{t('section_title')}</div>
|
||||
<div className="mt-2 text-lg text-muted-foreground">
|
||||
{t('section_description')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-x-3 gap-y-8 md:grid-cols-3">
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg
|
||||
className="stroke-primary-foreground stroke-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3M12 12l8-4.5M12 12v9M12 12L4 7.5" />
|
||||
</svg>
|
||||
}
|
||||
title={t('feature1_title')}
|
||||
>
|
||||
{t('feature_description')}
|
||||
</FeatureCard>
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg
|
||||
className="stroke-primary-foreground stroke-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3M12 12l8-4.5M12 12v9M12 12L4 7.5" />
|
||||
</svg>
|
||||
}
|
||||
title={t('feature2_title')}
|
||||
>
|
||||
{t('feature_description')}
|
||||
</FeatureCard>
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg
|
||||
className="stroke-primary-foreground stroke-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3M12 12l8-4.5M12 12v9M12 12L4 7.5" />
|
||||
</svg>
|
||||
}
|
||||
title={t('feature3_title')}
|
||||
>
|
||||
{t('feature_description')}
|
||||
</FeatureCard>
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg
|
||||
className="stroke-primary-foreground stroke-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3M12 12l8-4.5M12 12v9M12 12L4 7.5" />
|
||||
</svg>
|
||||
}
|
||||
title={t('feature4_title')}
|
||||
>
|
||||
{t('feature_description')}
|
||||
</FeatureCard>
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg
|
||||
className="stroke-primary-foreground stroke-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3M12 12l8-4.5M12 12v9M12 12L4 7.5" />
|
||||
</svg>
|
||||
}
|
||||
title={t('feature5_title')}
|
||||
>
|
||||
{t('feature_description')}
|
||||
</FeatureCard>
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg
|
||||
className="stroke-primary-foreground stroke-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3M12 12l8-4.5M12 12v9M12 12L4 7.5" />
|
||||
</svg>
|
||||
}
|
||||
title={t('feature6_title')}
|
||||
>
|
||||
{t('feature_description')}
|
||||
</FeatureCard>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
type FeatureCardProps = {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const FeatureCard = ({ icon, title, children }: FeatureCardProps) => (
|
||||
<div className="rounded-xl border border-border bg-card p-5">
|
||||
<div className="size-12 rounded-lg bg-gradient-to-br from-indigo-400 via-purple-400 to-pink-400 p-2 [&_svg]:stroke-white [&_svg]:stroke-2">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="mt-2 text-lg font-bold">{title}</div>
|
||||
<div className="my-3 w-8 border-t border-purple-400" />
|
||||
<div className="mt-2 text-muted-foreground">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,23 +1,24 @@
|
|||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { CenteredHero } from '@/components/landing/CenteredHero';
|
||||
import { Section } from '@/components/landing/Section';
|
||||
import { Section } from '@/components/layout/Section';
|
||||
|
||||
export const Hero = () => {
|
||||
const t = useTranslations('Hero');
|
||||
|
||||
return (
|
||||
<Section className="py-36">
|
||||
<CenteredHero
|
||||
title={t.rich('title', {
|
||||
<div className="mt-3 text-center text-5xl font-bold tracking-tight">
|
||||
{t.rich('title', {
|
||||
important: (chunks) => (
|
||||
<span className="bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
{chunks}
|
||||
</span>
|
||||
),
|
||||
})}
|
||||
description={t('description')}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-auto mt-5 max-w-screen-md text-center text-xl text-muted-foreground">
|
||||
{t('description')}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const Section = (props: {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
}) => (
|
||||
<div className={cn('px-3 py-16', props.className)}>
|
||||
{(props.title || props.subtitle || props.description) && (
|
||||
<div className="mx-auto mb-12 max-w-screen-md text-center">
|
||||
{props.subtitle && (
|
||||
<div className="bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 bg-clip-text text-sm font-bold text-transparent">
|
||||
{props.subtitle}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.title && (
|
||||
<div className="mt-1 text-3xl font-bold">{props.title}</div>
|
||||
)}
|
||||
|
||||
{props.description && (
|
||||
<div className="mt-2 text-lg text-muted-foreground">
|
||||
{props.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mx-auto max-w-screen-lg">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
2
src/components/landing/index.ts
Normal file
2
src/components/landing/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { Hero } from './Hero';
|
||||
export { Features } from './Features';
|
||||
52
src/components/layout/Footer.tsx
Normal file
52
src/components/layout/Footer.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Section } from '@/components/layout';
|
||||
import { APP_NAME } from '@/lib/constants';
|
||||
|
||||
export const Footer = () => {
|
||||
const t = useTranslations('Footer');
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<ul className="mt-4 flex flex-row gap-x-5 text-muted-foreground [&_svg:hover]:text-primary [&_svg:hover]:opacity-100 [&_svg]:size-5 [&_svg]:fill-current [&_svg]:opacity-60">
|
||||
<li>
|
||||
<Link href="/">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.954 4.569a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.691 8.094 4.066 6.13 1.64 3.161a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.061a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.937 4.937 0 004.604 3.417 9.868 9.868 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63a9.936 9.936 0 002.46-2.548l-.047-.02z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-6 flex w-full items-center justify-between gap-y-2 border-t pt-3 text-sm text-muted-foreground max-md:flex-col">
|
||||
<div>
|
||||
© Copyright {new Date().getFullYear()} {APP_NAME}.
|
||||
</div>
|
||||
<ul className="flex gap-x-4 font-medium [&_a:hover]:opacity-100 [&_a]:opacity-60">
|
||||
<li>
|
||||
<Link href="/sign-up">{t('terms_of_service')}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/sign-up">{t('privacy_policy')}</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
|
@ -9,10 +9,10 @@ import { setLocale } from '@/actions/locale';
|
|||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui';
|
||||
import { Globe } from 'lucide-react';
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { APP_NAME } from '@/lib/constants';
|
||||
|
||||
export const Logo = () => (
|
||||
<div className="flex items-center text-xl font-semibold">{APP_NAME}</div>
|
||||
<div className="text-lg font-semibold">{APP_NAME}</div>
|
||||
);
|
||||
41
src/components/layout/Navbar.tsx
Normal file
41
src/components/layout/Navbar.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Button } from '@/components/ui';
|
||||
import { Section, LocaleSwitcher, ThemeSwitcher } from '@/components/layout';
|
||||
|
||||
import { Logo } from './Logo';
|
||||
|
||||
export const Navbar = () => {
|
||||
const t = useTranslations('Navbar');
|
||||
|
||||
return (
|
||||
<Section className="py-6">
|
||||
<div className="flex flex-wrap items-center justify-between">
|
||||
<Link href="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
<ul className="flex flex-row items-center gap-x-1.5 text-lg font-medium [&_li[data-fade]:hover]:opacity-100 [&_li[data-fade]]:opacity-60">
|
||||
<li data-fade>
|
||||
<LocaleSwitcher />
|
||||
</li>
|
||||
<li data-fade>
|
||||
<ThemeSwitcher />
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="ghost" asChild>
|
||||
<Link href="/sign-in">{t('sign_in')}</Link>
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button asChild>
|
||||
<Link href="/sign-up">{t('sign_up')}</Link>
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
9
src/components/layout/Section.tsx
Normal file
9
src/components/layout/Section.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
type SectionProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const Section = ({ className, children, ...rest }: SectionProps) => (
|
||||
<div className={cn('px-4 py-16', className)} {...rest}>
|
||||
<div className="mx-auto max-w-screen-lg">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -6,13 +6,14 @@ import { useTheme } from 'next-themes';
|
|||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
} from '@/components/ui';
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
const { setTheme } = useTheme();
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
|
|
@ -23,15 +24,11 @@ export function ThemeSwitcher() {
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
|
||||
<DropdownMenuRadioItem value="light">Light</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="dark">Dark</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="system">System</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
6
src/components/layout/index.ts
Normal file
6
src/components/layout/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export { Navbar } from './Navbar';
|
||||
export { Footer } from './Footer';
|
||||
export { Section } from './Section';
|
||||
export { LocaleSwitcher } from './LocaleSwitcher';
|
||||
export { ThemeSwitcher } from './ThemeSwitcher';
|
||||
export { Logo } from './Logo';
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { FeatureCard } from '@/components/landing/FeatureCard';
|
||||
import { Section } from '@/components/landing/Section';
|
||||
|
||||
export const Features = () => {
|
||||
const t = useTranslations('Features');
|
||||
|
||||
return (
|
||||
<div className="w-full bg-secondary">
|
||||
<Section
|
||||
subtitle={t('section_subtitle')}
|
||||
title={t('section_title')}
|
||||
description={t('section_description')}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-x-3 gap-y-8 md:grid-cols-3">
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg
|
||||
className="stroke-primary-foreground stroke-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3M12 12l8-4.5M12 12v9M12 12L4 7.5" />
|
||||
</svg>
|
||||
}
|
||||
title={t('feature1_title')}
|
||||
>
|
||||
{t('feature_description')}
|
||||
</FeatureCard>
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg
|
||||
className="stroke-primary-foreground stroke-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3M12 12l8-4.5M12 12v9M12 12L4 7.5" />
|
||||
</svg>
|
||||
}
|
||||
title={t('feature2_title')}
|
||||
>
|
||||
{t('feature_description')}
|
||||
</FeatureCard>
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg
|
||||
className="stroke-primary-foreground stroke-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3M12 12l8-4.5M12 12v9M12 12L4 7.5" />
|
||||
</svg>
|
||||
}
|
||||
title={t('feature3_title')}
|
||||
>
|
||||
{t('feature_description')}
|
||||
</FeatureCard>
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg
|
||||
className="stroke-primary-foreground stroke-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3M12 12l8-4.5M12 12v9M12 12L4 7.5" />
|
||||
</svg>
|
||||
}
|
||||
title={t('feature4_title')}
|
||||
>
|
||||
{t('feature_description')}
|
||||
</FeatureCard>
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg
|
||||
className="stroke-primary-foreground stroke-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3M12 12l8-4.5M12 12v9M12 12L4 7.5" />
|
||||
</svg>
|
||||
}
|
||||
title={t('feature5_title')}
|
||||
>
|
||||
{t('feature_description')}
|
||||
</FeatureCard>
|
||||
<FeatureCard
|
||||
icon={
|
||||
<svg
|
||||
className="stroke-primary-foreground stroke-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" />
|
||||
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3M12 12l8-4.5M12 12v9M12 12L4 7.5" />
|
||||
</svg>
|
||||
}
|
||||
title={t('feature6_title')}
|
||||
>
|
||||
{t('feature_description')}
|
||||
</FeatureCard>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { CenteredFooter } from '@/components/landing/CenteredFooter';
|
||||
import { Section } from '@/components/landing/Section';
|
||||
|
||||
export const Footer = () => {
|
||||
const t = useTranslations('Footer');
|
||||
|
||||
return (
|
||||
<Section className="pb-16 pt-0">
|
||||
<CenteredFooter
|
||||
iconList={
|
||||
<>
|
||||
<li>
|
||||
<Link href="/">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.954 4.569a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.691 8.094 4.066 6.13 1.64 3.161a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.061a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.937 4.937 0 004.604 3.417 9.868 9.868 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63a9.936 9.936 0 002.46-2.548l-.047-.02z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
}
|
||||
legalLinks={
|
||||
<>
|
||||
<li>
|
||||
<Link href="/sign-up">{t('terms_of_service')}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/sign-up">{t('privacy_policy')}</Link>
|
||||
</li>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Button } from '@/components/ui';
|
||||
import { LocaleSwitcher } from '@/components/LocaleSwitcher';
|
||||
import { ThemeSwitcher } from '@/components/ThemeSwitcher';
|
||||
import { CenteredMenu } from '@/components/landing/CenteredMenu';
|
||||
import { Section } from '@/components/landing/Section';
|
||||
|
||||
import { Logo } from './Logo';
|
||||
|
||||
export const Navbar = () => {
|
||||
const t = useTranslations('Navbar');
|
||||
|
||||
return (
|
||||
<Section className="px-3 py-6">
|
||||
<CenteredMenu
|
||||
logo={<Logo />}
|
||||
rightMenu={
|
||||
<>
|
||||
<li data-fade>
|
||||
<LocaleSwitcher />
|
||||
</li>
|
||||
<li data-fade>
|
||||
<ThemeSwitcher />
|
||||
</li>
|
||||
<li className="ml-1 mr-2.5" data-fade>
|
||||
<Link href="/sign-in">{t('sign_in')}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Button asChild>
|
||||
<Link href="/sign-up">{t('sign_up')}</Link>
|
||||
</Button>
|
||||
</li>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
|
@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,8 @@ function DropdownMenuItem({
|
|||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
'cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -92,7 +93,8 @@ function DropdownMenuCheckboxItem({
|
|||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
'cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
|
|
@ -128,7 +130,8 @@ function DropdownMenuRadioItem({
|
|||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
'cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -211,7 +214,8 @@ function DropdownMenuSubTrigger({
|
|||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
|
||||
'cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Env } from '../lib/server/Env';
|
||||
import { Env } from '../lib/server/env';
|
||||
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
import { act, renderHook } from '@testing-library/react';
|
||||
|
||||
import { useMenu } from './useMenu';
|
||||
|
||||
describe('UseMenu', () => {
|
||||
describe('Render hook', () => {
|
||||
it("shouldn't show the menu by default", async () => {
|
||||
const { result } = renderHook(() => useMenu());
|
||||
|
||||
expect(result.current.showMenu).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should make the menu visible by toggling the menu', () => {
|
||||
const { result } = renderHook(() => useMenu());
|
||||
|
||||
act(() => {
|
||||
result.current.handleToggleMenu();
|
||||
});
|
||||
|
||||
expect(result.current.showMenu).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shouldn't make the menu visible after toggling and closing the menu", () => {
|
||||
const { result } = renderHook(() => useMenu());
|
||||
|
||||
act(() => {
|
||||
result.current.handleClose();
|
||||
});
|
||||
|
||||
expect(result.current.showMenu).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shouldn't make the menu visible after toggling the menu twice", () => {
|
||||
const { result } = renderHook(() => useMenu());
|
||||
|
||||
act(() => {
|
||||
result.current.handleToggleMenu();
|
||||
result.current.handleToggleMenu();
|
||||
});
|
||||
|
||||
expect(result.current.showMenu).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
/**
|
||||
* React Hook to toggle element. Mostly used for responsive menu.
|
||||
* @hook
|
||||
*/
|
||||
export const useMenu = () => {
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
|
||||
const handleToggleMenu = () => {
|
||||
setShowMenu((prevState) => !prevState);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setShowMenu(false);
|
||||
};
|
||||
|
||||
return { showMenu, handleToggleMenu, handleClose };
|
||||
};
|
||||
|
|
@ -3,7 +3,7 @@ import logtail from '@logtail/pino';
|
|||
import pino from 'pino';
|
||||
import pretty from 'pino-pretty';
|
||||
|
||||
import { Env } from './Env';
|
||||
import { Env } from './env';
|
||||
|
||||
let stream: DestinationStream;
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ if (Env.LOGTAIL_SOURCE_TOKEN) {
|
|||
sourceToken: Env.LOGTAIL_SOURCE_TOKEN,
|
||||
options: { sendLogsToBetterStack: true },
|
||||
}),
|
||||
{ stream: pretty() }, // Prints logs to the console
|
||||
{ stream: pretty() }, // Prints logs to the console.
|
||||
]);
|
||||
} else {
|
||||
stream = pretty({ colorize: true });
|
||||
Loading…
Add table
Reference in a new issue