More cleanup

This commit is contained in:
cte 2025-05-11 22:56:45 -07:00
parent 282143612f
commit 4ea155cf33
53 changed files with 487 additions and 913 deletions

60
.vscode/settings.json vendored
View file

@ -1,57 +1,17 @@
{
"editor.tabSize": 2,
"editor.detectIndentation": false,
"search.exclude": {
"package-lock.json": true
},
// TypeScript
"typescript.tsdk": "node_modules/typescript/lib", // Use the workspace version of TypeScript
"typescript.enablePromptUseWorkspaceTsdk": true, // For security reasons it's require that users opt into using the workspace version of typescript
"typescript.preferences.autoImportFileExcludePatterns": [
// useRouter should be imported from `next/navigation` instead of `next/router`
"next/router.d.ts",
"next/dist/client/router.d.ts",
// give priority for Link to next/link instead of lucide-react
"lucide-react"
],
"typescript.preferences.preferTypeOnlyAutoImports": true,
// Vitest
"testing.automaticallyOpenTestResults": "neverOpen", // Don't open the test results automatically
"prettier.enable": true,
"editor.formatOnSave": false,
// Auto fix with ESLint on save
// "editor.codeActionsOnSave": [
// "source.addMissingImports",
// "source.fixAll.eslint"
// ],
"search.exclude": {
"pnpm-lock.yaml": true
},
// Enable eslint for all supported languages
"eslint.format.enable": false,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"css",
"less",
"scss",
"pcss",
"postcss",
"github-actions-workflow"
]
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.preferences.preferTypeOnlyAutoImports": true,
"testing.automaticallyOpenTestResults": "neverOpen",
"prettier.enable": true
}

9
src/actions/locale.ts Normal file
View file

@ -0,0 +1,9 @@
'use server';
import type { Locale } from '@/types/locale';
import { cookies } from 'next/headers';
export const setLocale = async (locale: Locale) => {
const store = await cookies();
store.set('locale', locale);
};

View file

@ -1,7 +1,7 @@
import { useTranslations } from 'next-intl';
import { getLocale, getTranslations } from 'next-intl/server';
import { DashboardHeader } from '@/features/dashboard/DashboardHeader';
import { DashboardHeader } from '@/components/dashboard/DashboardHeader';
export async function generateMetadata() {
const locale = await getLocale();

View file

@ -1,7 +1,7 @@
import { OrganizationProfile } from '@clerk/nextjs';
import { useTranslations } from 'next-intl';
import { TitleBar } from '@/features/dashboard/TitleBar';
import { TitleBar } from '@/components/dashboard/TitleBar';
const OrganizationProfilePage = () => {
const t = useTranslations('OrganizationProfile');

View file

@ -1,7 +1,7 @@
import { useTranslations } from 'next-intl';
import { MessageState } from '@/features/dashboard/MessageState';
import { TitleBar } from '@/features/dashboard/TitleBar';
import { MessageState } from '@/components/dashboard/MessageState';
import { TitleBar } from '@/components/dashboard/TitleBar';
const DashboardIndexPage = () => {
const t = useTranslations('DashboardIndex');

View file

@ -1,7 +1,7 @@
import { UserProfile } from '@clerk/nextjs';
import { useTranslations } from 'next-intl';
import { TitleBar } from '@/features/dashboard/TitleBar';
import { TitleBar } from '@/components/dashboard/TitleBar';
const UserProfilePage = () => {
const t = useTranslations('UserProfile');

View file

@ -1,9 +1,9 @@
import { getLocale, getTranslations, setRequestLocale } from 'next-intl/server';
import { Features } from '@/templates/Features';
import { Footer } from '@/templates/Footer';
import { Hero } from '@/templates/Hero';
import { Navbar } from '@/templates/Navbar';
import { Features } from '@/components/templates/Features';
import { Footer } from '@/components/templates/Footer';
import { Hero } from '@/components/templates/Hero';
import { Navbar } from '@/components/templates/Navbar';
export async function generateMetadata() {
const locale = await getLocale();

View file

@ -1,6 +1,6 @@
import type { MetadataRoute } from 'next';
import { getBaseUrl } from '@/utils/Helpers';
import { getBaseUrl } from '@/lib/getBaseUrl';
export default function robots(): MetadataRoute.Robots {
return {

View file

@ -1,6 +1,6 @@
import type { MetadataRoute } from 'next';
import { getBaseUrl } from '@/utils/Helpers';
import { getBaseUrl } from '@/lib/getBaseUrl';
export default function sitemap(): MetadataRoute.Sitemap {
return [

View file

@ -3,24 +3,26 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@/utils/Helpers';
import { cn } from '@/lib/utils';
export const ActiveLink = (props: {
type ActiveLinkProps = {
href: string;
children: React.ReactNode;
}) => {
};
export const ActiveLink = ({ href, children }: ActiveLinkProps) => {
const pathname = usePathname();
return (
<Link
href={props.href}
href={href}
className={cn(
'px-3 py-2',
pathname.endsWith(props.href) &&
pathname.endsWith(href) &&
'rounded-md bg-primary text-primary-foreground',
)}
>
{props.children}
{children}
</Link>
);
};

View file

@ -1,28 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Background } from './Background';
const meta = {
title: 'Components/Background',
component: Background,
parameters: {
layout: 'fullscreen',
},
tags: ['autodocs'],
} satisfies Meta<typeof Background>;
export default meta;
type Story = StoryObj<typeof meta>;
export const DefaultBackgroundWithChildren = {
args: {
children: <div>Children node</div>,
},
} satisfies Story;
export const RedBackgroundWithChildren = {
args: {
className: 'bg-red-500',
children: <div>Children node</div>,
},
} satisfies Story;

View file

@ -1,10 +0,0 @@
import { cn } from '@/utils/Helpers';
export const Background = (props: {
children: React.ReactNode;
className?: string;
}) => (
<div className={cn('w-full bg-secondary', props.className)}>
{props.children}
</div>
);

View file

@ -1,10 +0,0 @@
export const DemoBadge = () => (
<div className="fixed bottom-0 right-20 z-10">
<a href="https://react-saas.com">
<div className="rounded-md bg-gray-900 px-3 py-2 font-semibold text-gray-100">
<span className="text-gray-500">Demo of</span>
{' SaaS Boilerplate'}
</div>
</a>
</div>
);

View file

@ -1,8 +1,11 @@
'use client';
import { usePathname, useRouter } from 'next/navigation';
import { useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { useLocale } from 'next-intl';
import { Locales, isLocale } from '@/types/locale';
import { setLocale } from '@/actions/locale';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@ -11,17 +14,20 @@ import {
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { AppConfig } from '@/utils/AppConfig';
export const LocaleSwitcher = () => {
const router = useRouter();
const pathname = usePathname();
const locale = useLocale();
const handleChange = () => {
router.push(pathname);
router.refresh();
};
const onLocaleChange = useCallback(
(value: string) => {
if (isLocale(value)) {
setLocale(value);
router.refresh();
}
},
[router],
);
return (
<DropdownMenu>
@ -47,10 +53,10 @@ export const LocaleSwitcher = () => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuRadioGroup value={locale} onValueChange={handleChange}>
{AppConfig.locales.map((elt) => (
<DropdownMenuRadioItem key={elt.id} value={elt.id}>
{elt.name}
<DropdownMenuRadioGroup value={locale} onValueChange={onLocaleChange}>
{Object.entries(Locales).map(([id, name]) => (
<DropdownMenuRadioItem key={id} value={id}>
{name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>

View file

@ -2,16 +2,12 @@ import { type ForwardedRef, forwardRef } from 'react';
import { Button } from '@/components/ui/button';
/**
* A toggle button to show/hide component in small screen.
* @component
* @params props - Component props.
* @params props.onClick - Function to run when the button is clicked.
*/
type ToggleMenuButtonProps = {
onClick?: () => void;
};
const ToggleMenuButtonInternal = (
props: {
onClick?: () => void;
},
props: ToggleMenuButtonProps,
ref?: ForwardedRef<HTMLButtonElement>,
) => (
<Button

View file

@ -0,0 +1,96 @@
'use client';
import { OrganizationSwitcher, UserButton } from '@clerk/nextjs';
import Link from 'next/link';
import { ActiveLink } from '@/components/ActiveLink';
import { LocaleSwitcher } from '@/components/LocaleSwitcher';
import { ToggleMenuButton } from '@/components/ToggleMenuButton';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Separator } from '@/components/ui/separator';
import { Logo } from '@/components/templates/Logo';
type DashboardHeaderProps = {
menu: {
href: string;
label: string;
}[];
};
export const DashboardHeader = (props: DashboardHeaderProps) => (
<>
<div className="flex items-center">
<Link href="/dashboard" className="max-sm:hidden">
<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
organizationProfileMode="navigation"
organizationProfileUrl="/dashboard/organization-profile"
afterCreateOrganizationUrl="/dashboard"
hidePersonal
skipInvitationScreen
appearance={{
elements: { organizationSwitcherTrigger: 'max-w-28 sm:max-w-52' },
}}
/>
<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">
{props.menu.map((item) => (
<li key={item.href}>
<ActiveLink href={item.href}>{item.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>
{props.menu.map((item) => (
<DropdownMenuItem key={item.href} asChild>
<Link href={item.href}>{item.label}</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</li>
<li data-fade>
<LocaleSwitcher />
</li>
<li>
<Separator orientation="vertical" className="h-4" />
</li>
<li>
<UserButton
userProfileMode="navigation"
userProfileUrl="/dashboard/user-profile"
appearance={{ elements: { rootBox: 'px-2 py-1.5' } }}
/>
</li>
</ul>
</div>
</>
);

View file

@ -0,0 +1,34 @@
import React from 'react';
type CenteredFooterProps = {
name: string;
iconList: React.ReactNode;
legalLinks: React.ReactNode;
children?: React.ReactNode;
};
export const CenteredFooter = ({
name,
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()} {name}.
</div>
<ul className="flex gap-x-4 font-medium [&_a:hover]:opacity-100 [&_a]:opacity-60">
{legalLinks}
</ul>
</div>
</div>
);

View file

@ -0,0 +1,25 @@
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>
)}
</>
);

View file

@ -3,8 +3,8 @@
import Link from 'next/link';
import { ToggleMenuButton } from '@/components/ToggleMenuButton';
import { useMenu } from '@/hooks/UseMenu';
import { cn } from '@/utils/Helpers';
import { useMenu } from '@/hooks/useMenu';
import { cn } from '@/lib/utils';
export const CenteredMenu = (props: {
logo: React.ReactNode;

View file

@ -1,4 +1,4 @@
import { cn } from '@/utils/Helpers';
import { cn } from '@/lib/utils';
export const Section = (props: {
children: React.ReactNode;

View file

@ -1,14 +1,13 @@
import { useTranslations } from 'next-intl';
import { Background } from '@/components/Background';
import { FeatureCard } from '@/features/landing/FeatureCard';
import { Section } from '@/features/landing/Section';
import { FeatureCard } from '@/components/landing/FeatureCard';
import { Section } from '@/components/landing/Section';
export const Features = () => {
const t = useTranslations('Features');
return (
<Background>
<div className="w-full bg-secondary">
<Section
subtitle={t('section_subtitle')}
title={t('section_title')}
@ -33,7 +32,6 @@ export const Features = () => {
>
{t('feature_description')}
</FeatureCard>
<FeatureCard
icon={
<svg
@ -52,7 +50,6 @@ export const Features = () => {
>
{t('feature_description')}
</FeatureCard>
<FeatureCard
icon={
<svg
@ -71,7 +68,6 @@ export const Features = () => {
>
{t('feature_description')}
</FeatureCard>
<FeatureCard
icon={
<svg
@ -90,7 +86,6 @@ export const Features = () => {
>
{t('feature_description')}
</FeatureCard>
<FeatureCard
icon={
<svg
@ -109,7 +104,6 @@ export const Features = () => {
>
{t('feature_description')}
</FeatureCard>
<FeatureCard
icon={
<svg
@ -130,6 +124,6 @@ export const Features = () => {
</FeatureCard>
</div>
</Section>
</Background>
</div>
);
};

View file

@ -0,0 +1,53 @@
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { CenteredFooter } from '@/components/landing/CenteredFooter';
import { Section } from '@/components/landing/Section';
import { APP_CONFIG } from '@/lib/constants';
export const Footer = () => {
const t = useTranslations('Footer');
return (
<Section className="pb-16 pt-0">
<CenteredFooter
name={APP_CONFIG.name}
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>
);
};

View file

@ -0,0 +1,23 @@
import { useTranslations } from 'next-intl';
import { CenteredHero } from '@/components/landing/CenteredHero';
import { Section } from '@/components/landing/Section';
export const Hero = () => {
const t = useTranslations('Hero');
return (
<Section className="py-36">
<CenteredHero
title={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')}
/>
</Section>
);
};

View file

@ -0,0 +1,7 @@
import { APP_CONFIG } from '@/lib/constants';
export const Logo = () => (
<div className="flex items-center text-xl font-semibold">
{APP_CONFIG.name}
</div>
);

View file

@ -3,8 +3,8 @@ import { useTranslations } from 'next-intl';
import { LocaleSwitcher } from '@/components/LocaleSwitcher';
import { buttonVariants } from '@/components/ui/button';
import { CenteredMenu } from '@/features/landing/CenteredMenu';
import { Section } from '@/features/landing/Section';
import { CenteredMenu } from '@/components/landing/CenteredMenu';
import { Section } from '@/components/landing/Section';
import { Logo } from './Logo';

View file

@ -11,9 +11,9 @@ import { migrate as migratePglite } from 'drizzle-orm/pglite/migrator';
import { PHASE_PRODUCTION_BUILD } from 'next/dist/shared/lib/constants';
import { Client } from 'pg';
import * as schema from '@/models/Schema';
import * as schema from '@/db/schema';
import { Env } from './Env';
import { Env } from '../lib/server/Env';
let client;
let drizzle;

View file

@ -1,111 +0,0 @@
'use client';
import { OrganizationSwitcher, UserButton } from '@clerk/nextjs';
import Link from 'next/link';
import { ActiveLink } from '@/components/ActiveLink';
import { LocaleSwitcher } from '@/components/LocaleSwitcher';
import { ToggleMenuButton } from '@/components/ToggleMenuButton';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Separator } from '@/components/ui/separator';
import { Logo } from '@/templates/Logo';
export const DashboardHeader = (props: {
menu: {
href: string;
label: string;
}[];
}) => {
return (
<>
<div className="flex items-center">
<Link href="/dashboard" className="max-sm:hidden">
<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
organizationProfileMode="navigation"
organizationProfileUrl="/dashboard/organization-profile"
afterCreateOrganizationUrl="/dashboard"
hidePersonal
skipInvitationScreen
appearance={{
elements: {
organizationSwitcherTrigger: 'max-w-28 sm:max-w-52',
},
}}
/>
<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">
{props.menu.map((item) => (
<li key={item.href}>
<ActiveLink href={item.href}>{item.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>
{props.menu.map((item) => (
<DropdownMenuItem key={item.href} asChild>
<Link href={item.href}>{item.label}</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</li>
{/* PRO: Dark mode toggle button */}
<li data-fade>
<LocaleSwitcher />
</li>
<li>
<Separator orientation="vertical" className="h-4" />
</li>
<li>
<UserButton
userProfileMode="navigation"
userProfileUrl="/dashboard/user-profile"
appearance={{
elements: {
rootBox: 'px-2 py-1.5',
},
}}
/>
</li>
</ul>
</div>
</>
);
};

View file

@ -1,52 +0,0 @@
import { useTranslations } from 'next-intl';
import React from 'react';
export const CenteredFooter = (props: {
logo: React.ReactNode;
name: string;
iconList: React.ReactNode;
legalLinks: React.ReactNode;
children: React.ReactNode;
}) => {
const t = useTranslations('Footer');
return (
<div className="flex flex-col items-center text-center">
{props.logo}
<ul className="mt-4 flex gap-x-8 text-lg max-sm:flex-col [&_a:hover]:opacity-100 [&_a]:opacity-60">
{props.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">
{props.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()} ${props.name}. `}
{t.rich('designed_by', {
author: () => (
<a
className="text-blue-500 hover:text-blue-600"
href="https://creativedesignsguru.com"
>
Creative Designs Guru
</a>
),
})}
{/*
* PLEASE READ THIS SECTION
* I'm an indie maker with limited resources and funds, I'll really appreciate if you could have a link to my website.
* The link doesn't need to appear on every pages, one link on one page is enough.
* For example, in the `About` page. Thank you for your support, it'll mean a lot to me.
*/}
</div>
<ul className="flex gap-x-4 font-medium [&_a:hover]:opacity-100 [&_a]:opacity-60">
{props.legalLinks}
</ul>
</div>
</div>
);
};

View file

@ -1,22 +0,0 @@
export const CenteredHero = (props: {
banner: React.ReactNode;
title: React.ReactNode;
description: string;
buttons: React.ReactNode;
}) => (
<>
<div className="text-center">{props.banner}</div>
<div className="mt-3 text-center text-5xl font-bold tracking-tight">
{props.title}
</div>
<div className="mx-auto mt-5 max-w-screen-md text-center text-xl text-muted-foreground">
{props.description}
</div>
<div className="mt-8 flex justify-center gap-x-5 gap-y-3 max-sm:flex-col">
{props.buttons}
</div>
</>
);

View file

@ -1,6 +1,6 @@
import { act, renderHook } from '@testing-library/react';
import { useMenu } from './UseMenu';
import { useMenu } from './useMenu';
describe('UseMenu', () => {
describe('Render hook', () => {

75
src/i18n/locales/en.json Normal file
View file

@ -0,0 +1,75 @@
{
"Index": {
"meta_title": "SaaS Template - The perfect SaaS template to build and scale your business with ease.",
"meta_description": "A free and open-source landing page template for your SaaS business, built with React, TypeScript, Shadcn UI, and Tailwind CSS."
},
"Navbar": {
"sign_in": "Sign In",
"sign_up": "Sign Up",
"product": "Product",
"docs": "Docs",
"blog": "Blog",
"community": "Community",
"company": "Company"
},
"Hero": {
"title": "The perfect <important>SaaS template</important> to build and scale your business with ease.",
"description": "A free and open-source landing page template for your SaaS business, built with React, TypeScript, Shadcn UI, and Tailwind CSS."
},
"Features": {
"section_subtitle": "Features",
"section_title": "Unlock the Full Potential of the SaaS Template",
"section_description": "A free and open-source landing page template for your SaaS business, built with React, TypeScript, Shadcn UI, and Tailwind CSS.",
"feature1_title": "Node.js",
"feature2_title": "React",
"feature3_title": "Tailwind CSS",
"feature4_title": "TypeScript",
"feature5_title": "Shadcn UI",
"feature6_title": "ESLint",
"feature_description": "A free and open-source landing page template for your SaaS business, built with React, TypeScript, Shadcn UI, and Tailwind CSS."
},
"Footer": {
"product": "Product",
"docs": "Docs",
"blog": "Blog",
"community": "Community",
"company": "Company",
"terms_of_service": "Terms Of Service",
"privacy_policy": "Privacy Policy"
},
"SignIn": {
"meta_title": "Sign in",
"meta_description": "Seamlessly sign in to your account with our user-friendly login process."
},
"SignUp": {
"meta_title": "Sign up",
"meta_description": "Effortlessly create an account through our intuitive sign-up process."
},
"DashboardLayout": {
"home": "Home",
"todos": "Todos",
"members": "Members",
"billing": "Billing",
"settings": "Settings"
},
"Dashboard": {
"meta_title": "SaaS Template Dashboard",
"meta_description": "A free and open-source landing page template for your SaaS business, built with Next.js, TypeScript, Shadcn UI, and Tailwind CSS."
},
"DashboardIndex": {
"title_bar": "Dashboard",
"title_bar_description": "Welcome to your dashboard",
"message_state_title": "Let's get started",
"message_state_description": "You can customize this page by editing the file at <code>dashboard/page.tsx</code>",
"message_state_button": "Star on GitHub",
"message_state_alternative": "Want more features using the same stack? Try <url></url>."
},
"UserProfile": {
"title_bar": "User Profile",
"title_bar_description": "View and manage your user profile"
},
"OrganizationProfile": {
"title_bar": "Organization Management",
"title_bar_description": "Manage your organization"
}
}

75
src/i18n/locales/fr.json Normal file
View file

@ -0,0 +1,75 @@
{
"Index": {
"meta_title": "SaaS Template - Le template SaaS parfait pour construire et mettre à l'échelle votre entreprise en toute simplicité.",
"meta_description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec React, TypeScript, Shadcn UI et Tailwind CSS."
},
"Navbar": {
"sign_in": "Se connecter",
"sign_up": "S'inscrire",
"product": "Produit",
"docs": "Docs",
"blog": "Blog",
"community": "Communauté",
"company": "Entreprise"
},
"Hero": {
"title": "Le parfait <important>SaaS template</important> pour construire et mettre à l'échelle votre entreprise en toute simplicité.",
"description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec React, TypeScript, Shadcn UI et Tailwind CSS."
},
"Features": {
"section_subtitle": "Fonctionnalités",
"section_title": "Débloquer le plein potentiel du SaaS Template",
"section_description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec React, TypeScript, Shadcn UI et Tailwind CSS.",
"feature1_title": "Node.js",
"feature2_title": "React",
"feature3_title": "Tailwind CSS",
"feature4_title": "TypeScript",
"feature5_title": "Shadcn UI",
"feature6_title": "ESLint",
"feature_description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec React, TypeScript, Shadcn UI et Tailwind CSS."
},
"Footer": {
"product": "Produit",
"docs": "Docs",
"blog": "Blog",
"community": "Communauté",
"company": "Entreprise",
"terms_of_service": "Conditions d'utilisation",
"privacy_policy": "Politique de confidentialité"
},
"SignIn": {
"meta_title": "Se connecter",
"meta_description": "Connectez-vous à votre compte avec facilité."
},
"SignUp": {
"meta_title": "S'inscrire",
"meta_description": "Créez un compte sans effort grâce à notre processus d'inscription intuitif."
},
"DashboardLayout": {
"home": "Accueil",
"todos": "Todos",
"members": "Membres",
"billing": "Facturation",
"settings": "Réglages"
},
"Dashboard": {
"meta_title": "Tableau de bord du SaaS Template",
"meta_description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec Next.js, TypeScript, Shadcn UI et Tailwind CSS."
},
"DashboardIndex": {
"title_bar": "Tableau de bord",
"title_bar_description": "Bienvenue sur votre tableau de bord",
"message_state_title": "C'est parti",
"message_state_description": "Vous pouvez personnaliser cette page en modifiant le fichier dans <code>dashboard/page.tsx</code>",
"message_state_button": "Mettez une étoile sur GitHub",
"message_state_alternative": "Vous voulez plus de fonctionnalités en utilisant la même stack ? Essayez <url></url>."
},
"UserProfile": {
"title_bar": "Profil utilisateur",
"title_bar_description": "Afficher et gérer votre profil utilisateur"
},
"OrganizationProfile": {
"title_bar": "Gestion de lorganisation",
"title_bar_description": "Gérer votre organisation"
}
}

View file

@ -1,12 +1,16 @@
import { cookies } from 'next/headers';
import { isLocale } from '@/types/locale';
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async () => {
// Provide a static locale, fetch a user setting,
// read from `cookies()`, `headers()`, etc.
const locale = 'en';
export default getRequestConfig(async (params) => {
const requestLocale = await params.requestLocale;
const locale = isLocale(requestLocale)
? requestLocale
: ((await cookies()).get('locale')?.value ?? 'en');
return {
locale,
messages: (await import(`../locales/${locale}.json`)).default,
messages: (await import(`./locales/${locale}.json`)).default,
};
});

3
src/lib/constants.ts Normal file
View file

@ -0,0 +1,3 @@
export const APP_CONFIG = {
name: 'Roo Code Cloud',
};

View file

@ -1,11 +1,3 @@
import type { ClassValue } from 'clsx';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export const getBaseUrl = () => {
if (process.env.NEXT_PUBLIC_APP_URL) {
return process.env.NEXT_PUBLIC_APP_URL;

View file

@ -1,166 +0,0 @@
{
"Index": {
"meta_title": "SaaS Template - The perfect SaaS template to build and scale your business with ease.",
"meta_description": "A free and open-source landing page template for your SaaS business, built with React, TypeScript, Shadcn UI, and Tailwind CSS."
},
"Navbar": {
"sign_in": "Sign In",
"sign_up": "Sign Up",
"product": "Product",
"docs": "Docs",
"blog": "Blog",
"community": "Community",
"company": "Company"
},
"Hero": {
"follow_twitter": "Follow @Ixartz on Twitter",
"title": "The perfect <important>SaaS template</important> to build and scale your business with ease.",
"description": "A free and open-source landing page template for your SaaS business, built with React, TypeScript, Shadcn UI, and Tailwind CSS.",
"primary_button": "Get Started",
"secondary_button": "Star on GitHub"
},
"Sponsors": {
"title": "Sponsored by"
},
"Features": {
"section_subtitle": "Features",
"section_title": "Unlock the Full Potential of the SaaS Template",
"section_description": "A free and open-source landing page template for your SaaS business, built with React, TypeScript, Shadcn UI, and Tailwind CSS.",
"feature1_title": "Node.js",
"feature2_title": "React",
"feature3_title": "Tailwind CSS",
"feature4_title": "TypeScript",
"feature5_title": "Shadcn UI",
"feature6_title": "ESLint",
"feature_description": "A free and open-source landing page template for your SaaS business, built with React, TypeScript, Shadcn UI, and Tailwind CSS."
},
"Pricing": {
"section_subtitle": "Features",
"section_title": "Unlock the Full Potential of the SaaS Template",
"section_description": "A free and open-source landing page template for your SaaS business, built with React, TypeScript, Shadcn UI, and Tailwind CSS.",
"button_text": "Get Started"
},
"PricingPlan": {
"free_plan_name": "Free",
"premium_plan_name": "Premium",
"enterprise_plan_name": "Enterprise",
"free_plan_description": "For individuals",
"premium_plan_description": "For small teams",
"enterprise_plan_description": "For industry leaders",
"feature_team_member": "{number} Team Members",
"feature_website": "{number} Websites",
"feature_storage": "{number} GB Storage",
"feature_transfer": "{number} TB Transfer",
"feature_email_support": "Email Support",
"plan_interval_month": "month",
"plan_interval_year": "year",
"next_renew_date": "Your subscription renews on {date}"
},
"FAQ": {
"question": "Lorem ipsum dolor sit amet, consectetur adipiscing elit?",
"answer": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam aliquam enim dui, id consequat turpis ullamcorper ac. Mauris id quam dolor. Nullam eu egestas turpis. Proin risus elit, sollicitudin in mi a, accumsan euismod turpis. In euismod mi sed diam tristique hendrerit."
},
"CTA": {
"title": "You are ready?",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"button_text": "Star on GitHub"
},
"Footer": {
"product": "Product",
"docs": "Docs",
"blog": "Blog",
"community": "Community",
"company": "Company",
"terms_of_service": "Terms Of Service",
"privacy_policy": "Privacy Policy",
"designed_by": "Designed by <author></author>."
},
"ProtectFallback": {
"not_enough_permission": "You do not have the permissions to perform this action"
},
"SignIn": {
"meta_title": "Sign in",
"meta_description": "Seamlessly sign in to your account with our user-friendly login process."
},
"SignUp": {
"meta_title": "Sign up",
"meta_description": "Effortlessly create an account through our intuitive sign-up process."
},
"DashboardLayout": {
"home": "Home",
"todos": "Todos",
"members": "Members",
"billing": "Billing",
"settings": "Settings"
},
"Dashboard": {
"meta_title": "SaaS Template Dashboard",
"meta_description": "A free and open-source landing page template for your SaaS business, built with Next.js, TypeScript, Shadcn UI, and Tailwind CSS."
},
"DashboardIndex": {
"title_bar": "Dashboard",
"title_bar_description": "Welcome to your dashboard",
"message_state_title": "Let's get started",
"message_state_description": "You can customize this page by editing the file at <code>dashboard/page.tsx</code>",
"message_state_button": "Star on GitHub",
"message_state_alternative": "Want more features using the same stack? Try <url></url>."
},
"UserProfile": {
"title_bar": "User Profile",
"title_bar_description": "View and manage your user profile"
},
"OrganizationProfile": {
"title_bar": "Organization Management",
"title_bar_description": "Manage your organization"
},
"Billing": {
"title_bar": "Billing",
"title_bar_description": "Manage your billing and subscription",
"current_section_title": "Current Plan",
"current_section_description": "Adjust your payment plan to best suit your requirements",
"manage_subscription_button": "Manage Subscription"
},
"BillingOptions": {
"current_plan": "Current Plan",
"upgrade_plan": "Get Started"
},
"CheckoutConfirmation": {
"title_bar": "Payment Confirmation",
"message_state_title": "Payment successful",
"message_state_description": "Your payment has been successfully processed. Thank you for your purchase!",
"message_state_button": "Go back to Billing"
},
"DataTable": {
"no_results": "No results."
},
"Todos": {
"title_bar": "Todo List",
"title_bar_description": "View and manage your todo list",
"add_todo_button": "New todo"
},
"TodoTableColumns": {
"open_menu": "Open menu",
"edit": "Edit",
"delete": "Delete",
"title_header": "Title",
"message_header": "Message",
"created_at_header": "Created at"
},
"AddTodo": {
"title_bar": "Add Todo",
"add_todo_section_title": "Create a new todo",
"add_todo_section_description": "Fill in the form below to create a new todo"
},
"EditTodo": {
"title_bar": "Edit todo",
"edit_todo_section_title": "Modify todo",
"edit_todo_section_description": "Fill in the form below to edit the todo"
},
"TodoForm": {
"title_label": "Title",
"title_description": "Enter a descriptive title for your todo.",
"message_title": "Message",
"message_description": "Enter a detailed message for your todo.",
"submit_button": "Submit"
}
}

View file

@ -1,166 +0,0 @@
{
"Index": {
"meta_title": "SaaS Template - Le template SaaS parfait pour construire et mettre à l'échelle votre entreprise en toute simplicité.",
"meta_description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec React, TypeScript, Shadcn UI et Tailwind CSS."
},
"Navbar": {
"sign_in": "Se connecter",
"sign_up": "S'inscrire",
"product": "Produit",
"docs": "Docs",
"blog": "Blog",
"community": "Communauté",
"company": "Entreprise"
},
"Hero": {
"follow_twitter": "Suivez @Ixartz sur Twitter",
"title": "Le parfait <important>SaaS template</important> pour construire et mettre à l'échelle votre entreprise en toute simplicité.",
"description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec React, TypeScript, Shadcn UI et Tailwind CSS.",
"primary_button": "Démarrer",
"secondary_button": "Mettez une étoile sur GitHub"
},
"Sponsors": {
"title": "Sponsorisé par"
},
"Features": {
"section_subtitle": "Fonctionnalités",
"section_title": "Débloquer le plein potentiel du SaaS Template",
"section_description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec React, TypeScript, Shadcn UI et Tailwind CSS.",
"feature1_title": "Node.js",
"feature2_title": "React",
"feature3_title": "Tailwind CSS",
"feature4_title": "TypeScript",
"feature5_title": "Shadcn UI",
"feature6_title": "ESLint",
"feature_description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec React, TypeScript, Shadcn UI et Tailwind CSS."
},
"Pricing": {
"section_subtitle": "Fonctionnalités",
"section_title": "Débloquer le plein potentiel du SaaS Template",
"section_description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec React, TypeScript, Shadcn UI et Tailwind CSS.",
"button_text": "Démarrer"
},
"PricingPlan": {
"free_plan_name": "Gratuit",
"premium_plan_name": "Premium",
"enterprise_plan_name": "Enterprise",
"free_plan_description": "Pour les particuliers",
"premium_plan_description": "Pour les petites équipes",
"enterprise_plan_description": "Pour les leaders de l'industrie",
"feature_team_member": "{number} membres",
"feature_website": "{number} sites internet",
"feature_storage": "{number} Go de stockage",
"feature_transfer": "{number} TB de transfert",
"feature_email_support": "Support par e-mail",
"plan_interval_month": "mois",
"plan_interval_year": "année",
"next_renew_date": "Votre abonnement sera renouvelé le {date}"
},
"FAQ": {
"question": "Lorem ipsum dolor sit amet, consectetur adipiscing elit?",
"answer": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam aliquam enim dui, id consequat turpis ullamcorper ac. Mauris id quam dolor. Nullam eu egestas turpis. Proin risus elit, sollicitudin in mi a, accumsan euismod turpis. In euismod mi sed diam tristique hendrerit."
},
"CTA": {
"title": "Vous êtes prêt?",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"button_text": "Mettez une étoile sur GitHub"
},
"Footer": {
"product": "Produit",
"docs": "Docs",
"blog": "Blog",
"community": "Communauté",
"company": "Entreprise",
"terms_of_service": "Conditions d'utilisation",
"privacy_policy": "Politique de confidentialité",
"designed_by": "Designé by <author></author>."
},
"ProtectFallback": {
"not_enough_permission": "Vous n'avez pas les permissions pour effectuer cette action"
},
"SignIn": {
"meta_title": "Se connecter",
"meta_description": "Connectez-vous à votre compte avec facilité."
},
"SignUp": {
"meta_title": "S'inscrire",
"meta_description": "Créez un compte sans effort grâce à notre processus d'inscription intuitif."
},
"DashboardLayout": {
"home": "Accueil",
"todos": "Todos",
"members": "Membres",
"billing": "Facturation",
"settings": "Réglages"
},
"Dashboard": {
"meta_title": "Tableau de bord du SaaS Template",
"meta_description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec Next.js, TypeScript, Shadcn UI et Tailwind CSS."
},
"DashboardIndex": {
"title_bar": "Tableau de bord",
"title_bar_description": "Bienvenue sur votre tableau de bord",
"message_state_title": "C'est parti",
"message_state_description": "Vous pouvez personnaliser cette page en modifiant le fichier dans <code>dashboard/page.tsx</code>",
"message_state_button": "Mettez une étoile sur GitHub",
"message_state_alternative": "Vous voulez plus de fonctionnalités en utilisant la même stack ? Essayez <url></url>."
},
"UserProfile": {
"title_bar": "Profil utilisateur",
"title_bar_description": "Afficher et gérer votre profil utilisateur"
},
"OrganizationProfile": {
"title_bar": "Gestion de lorganisation",
"title_bar_description": "Gérer votre organisation"
},
"Billing": {
"title_bar": "Facturation",
"title_bar_description": "Gérer votre facturation et votre abonnement",
"current_section_title": "Plan actuel",
"current_section_description": "Ajuster votre plan de paiement pour le mieux répondre à vos besoins",
"manage_subscription_button": "Gérer l'abonnement"
},
"BillingOptions": {
"current_plan": "Plan actuel",
"upgrade_plan": "Démarrer"
},
"CheckoutConfirmation": {
"title_bar": "Confirmation du paiement",
"message_state_title": "Paiement accepté",
"message_state_description": "Votre paiement a été traité avec succès. Merci pour votre achat !",
"message_state_button": "Revenir à la facturation"
},
"DataTable": {
"no_results": "Aucun résultat."
},
"Todos": {
"title_bar": "Liste de Todos",
"title_bar_description": "Afficher et gérer votre liste de todos",
"add_todo_button": "Nouveau todo"
},
"TodoTableColumns": {
"open_menu": "Ouvrir le menu",
"edit": "Modifier",
"delete": "Supprimer",
"title_header": "Titre",
"message_header": "Message",
"created_at_header": "Créé le"
},
"AddTodo": {
"title_bar": "Ajouter une todo",
"add_todo_section_title": "Créer une nouvelle todo",
"add_todo_section_description": "Remplissez le formulaire ci-dessous pour créer une nouvelle todo"
},
"EditTodo": {
"title_bar": "Editer le todo",
"edit_todo_section_title": "Modifier le todo",
"edit_todo_section_description": "Remplissez le formulaire ci-dessous pour modifier la todo"
},
"TodoForm": {
"title_label": "Titre",
"title_description": "Entrez un titre descriptif pour votre todo.",
"message_title": "Message",
"message_description": "Entrez un message détaillé pour votre todo.",
"submit_button": "Envoyer"
}
}

View file

@ -1,110 +0,0 @@
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { CenteredFooter } from '@/features/landing/CenteredFooter';
import { Section } from '@/features/landing/Section';
import { AppConfig } from '@/utils/AppConfig';
import { Logo } from './Logo';
export const Footer = () => {
const t = useTranslations('Footer');
return (
<Section className="pb-16 pt-0">
<CenteredFooter
logo={<Logo />}
name={AppConfig.name}
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.998 12c0-6.628-5.372-12-11.999-12C5.372 0 0 5.372 0 12c0 5.988 4.388 10.952 10.124 11.852v-8.384H7.078v-3.469h3.046V9.356c0-3.008 1.792-4.669 4.532-4.669 1.313 0 2.686.234 2.686.234v2.953H15.83c-1.49 0-1.955.925-1.955 1.874V12h3.328l-.532 3.469h-2.796v8.384c5.736-.9 10.124-5.864 10.124-11.853z" />
</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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z" />
</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>
<li>
<Link href="/">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M11.585 5.267c1.834 0 3.558.811 4.824 2.08v.004c0-.609.41-1.068.979-1.068h.145c.891 0 1.073.842 1.073 1.109l.005 9.475c-.063.621.64.941 1.029.543 1.521-1.564 3.342-8.038-.946-11.79-3.996-3.497-9.357-2.921-12.209-.955-3.031 2.091-4.971 6.718-3.086 11.064 2.054 4.74 7.931 6.152 11.424 4.744 1.769-.715 2.586 1.676.749 2.457-2.776 1.184-10.502 1.064-14.11-5.188C-.977 13.521-.847 6.093 5.62 2.245 10.567-.698 17.09.117 21.022 4.224c4.111 4.294 3.872 12.334-.139 15.461-1.816 1.42-4.516.037-4.498-2.031l-.019-.678c-1.265 1.256-2.948 1.988-4.782 1.988-3.625 0-6.813-3.189-6.813-6.812 0-3.659 3.189-6.885 6.814-6.885zm4.561 6.623c-.137-2.653-2.106-4.249-4.484-4.249h-.09c-2.745 0-4.268 2.159-4.268 4.61 0 2.747 1.842 4.481 4.256 4.481 2.693 0 4.464-1.973 4.592-4.306l-.006-.536z" />
</svg>
</Link>
</li>
<li>
<Link href="/">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19.199 24C19.199 13.467 10.533 4.8 0 4.8V0c13.165 0 24 10.835 24 24h-4.801zM3.291 17.415a3.3 3.3 0 013.293 3.295A3.303 3.303 0 013.283 24C1.47 24 0 22.526 0 20.71s1.475-3.294 3.291-3.295zM15.909 24h-4.665c0-6.169-5.075-11.245-11.244-11.245V8.09c8.727 0 15.909 7.184 15.909 15.91z" />
</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>
</>
}
>
<li>
<Link href="/sign-up">{t('product')}</Link>
</li>
<li>
<Link href="/sign-up">{t('docs')}</Link>
</li>
<li>
<Link href="/sign-up">{t('blog')}</Link>
</li>
<li>
<Link href="/sign-up">{t('community')}</Link>
</li>
<li>
<Link href="/sign-up">{t('company')}</Link>
</li>
</CenteredFooter>
</Section>
);
};

View file

@ -1,52 +0,0 @@
import { GitHubLogoIcon, TwitterLogoIcon } from '@radix-ui/react-icons';
import { useTranslations } from 'next-intl';
import { buttonVariants } from '@/components/ui/button';
import { CenteredHero } from '@/features/landing/CenteredHero';
import { Section } from '@/features/landing/Section';
export const Hero = () => {
const t = useTranslations('Hero');
return (
<Section className="py-36">
<CenteredHero
banner={
<a
href="https://twitter.com/ixartz"
target="_blank"
rel="noopener noreferrer"
>
<TwitterLogoIcon className="mr-1 size-5" /> {t('follow_twitter')}
</a>
}
title={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')}
buttons={
<>
<a
className={buttonVariants({ size: 'lg' })}
href="https://github.com/ixartz/SaaS-Boilerplate"
>
{t('primary_button')}
</a>
<a
className={buttonVariants({ variant: 'outline', size: 'lg' })}
href="https://github.com/ixartz/SaaS-Boilerplate"
>
<GitHubLogoIcon className="mr-2 size-5" />
{t('secondary_button')}
</a>
</>
}
/>
</Section>
);
};

View file

@ -1,21 +0,0 @@
import { AppConfig } from '@/utils/AppConfig';
export const Logo = (props: { isTextHidden?: boolean }) => (
<div className="flex items-center text-xl font-semibold">
<svg
className="mr-1 size-8 stroke-current 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" />
<rect x="3" y="12" width="6" height="8" rx="1" />
<rect x="9" y="8" width="6" height="12" rx="1" />
<rect x="15" y="4" width="6" height="16" rx="1" />
<path d="M4 20h14" />
</svg>
{!props.isTextHidden && AppConfig.name}
</div>
);

View file

@ -1,4 +1,4 @@
import type { EnumValues } from './Enum';
import type { EnumValues } from './enum';
export const ORG_ROLE = {
ADMIN: 'org:admin',
@ -8,7 +8,7 @@ export const ORG_ROLE = {
export type OrgRole = EnumValues<typeof ORG_ROLE>;
export const ORG_PERMISSION = {
// Add Organization Permissions here
// Add Organization Permissions here.
} as const;
export type OrgPermission = EnumValues<typeof ORG_PERMISSION>;

View file

@ -1,4 +1,4 @@
import type { EnumValues } from './Enum';
import type { EnumValues } from './enum';
export const PLAN_ID = {
FREE: 'free',
@ -20,41 +20,9 @@ export const SUBSCRIPTION_STATUS = {
PENDING: 'pending',
} as const;
// PricingPlan is currently only used for Pricing section of the landing page.
// If you need a real Stripe subscription payment with checkout page, customer portal, webhook, etc.
// You can check out the Next.js Boilerplate Pro at: https://nextjs-boilerplate.com/pro-saas-starter-kit
// On top of that, you'll get access to real example of SaaS application with Next.js, TypeScript, Tailwind CSS, and more.
// You can find a live demo at: https://pro-demo.nextjs-boilerplate.com
export type PricingPlan = {
id: PlanId;
price: number;
interval: BillingInterval;
testPriceId: string; // Use for testing
devPriceId: string;
prodPriceId: string;
features: {
teamMember: number;
website: number;
storage: number;
transfer: number;
};
};
export type IStripeSubscription = {
stripeSubscriptionId: string | null;
stripeSubscriptionPriceId: string | null;
stripeSubscriptionStatus: string | null;
stripeSubscriptionCurrentPeriodEnd: number | null;
};
export type PlanDetails =
| {
isPaid: true;
plan: PricingPlan;
stripeDetails: IStripeSubscription;
}
| {
isPaid: false;
plan: PricingPlan;
stripeDetails?: undefined;
};

View file

@ -1,4 +1,4 @@
import type { OrgPermission, OrgRole } from '@/types/Auth';
import type { OrgPermission, OrgRole } from '@/types/auth';
declare global {
interface ClerkAuthorization {

11
src/types/locale.ts Normal file
View file

@ -0,0 +1,11 @@
export const locales = ['en', 'fr'] as const;
export type Locale = (typeof locales)[number];
export const isLocale = (value: string | undefined): value is Locale =>
locales.includes(value as Locale);
export const Locales: Record<Locale, string> = {
en: 'English',
fr: 'Français',
};

View file

@ -1,11 +0,0 @@
export const AppConfig = {
name: 'Roo Code Cloud',
locales: [
{
id: 'en',
name: 'English',
},
{ id: 'fr', name: 'Français' },
],
defaultLocale: 'en',
};