feat: remixify and whatever else needed to be done

This commit is contained in:
BlankParticle
2025-05-14 14:14:36 +05:30
parent acc680020f
commit fa45abaf44
227 changed files with 16591 additions and 2021 deletions

View File

@@ -1,5 +1,5 @@
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_BACKEND_URL=http://localhost:8787
VITE_PUBLIC_APP_URL=http://localhost:3000
VITE_PUBLIC_BACKEND_URL=http://localhost:8787
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/zerodotemail"

View File

@@ -29,13 +29,19 @@ Thank you for your interest in contributing to 0.email! We're excited to have yo
- Clone your fork locally: `git clone https://github.com/YOUR-USERNAME/Zero.git`
2. **Set Up Development Environment**
<<<<<<< HEAD
- Install [Bun](https://bun.sh)
- Clone the repository and install dependencies: `bun install`
- Start the database locally: `bun docker:db:up`
=======
- Install [pnpm](https://pnpm.io)
- Clone the repository and install dependencies: `pnpm install`
- Start the database locally: `pnpm docker:up`
>>>>>>> 7fb24724 (feat: remixify and whatever else needed to be done)
- Copy `.env.example` to `.env` in project root
- Setup cloudflare with `bun run cf-install`, you will need to run this everytime there is a `.env` change
- Setup cloudflare with `pnpm run cf-install`, you will need to run this everytime there is a `.env` change
- Set up your Google OAuth credentials (see [README.md](../README.md))
- Initialize the database: `bun db:push`
- Initialize the database: `pnpm db:push`
## Development Workflow
@@ -43,10 +49,14 @@ Thank you for your interest in contributing to 0.email! We're excited to have yo
```bash
# Start database locally
<<<<<<< HEAD
bun docker:db:up
=======
pnpm docker:up
>>>>>>> 7fb24724 (feat: remixify and whatever else needed to be done)
# Start the development server
bun dev
pnpm dev
```
2. **Create a New Branch**
@@ -121,16 +131,16 @@ Zero uses PostgreSQL with Drizzle ORM. Here's how to work with it:
```bash
# Apply schema changes to development database
bun db:push
pnpm db:push
# Create migration files after schema changes
bun db:generate
pnpm db:generate
# Apply migrations (for production)
bun db:migrate
pnpm db:migrate
# View and edit data with Drizzle Studio
bun db:studio
pnpm db:studio
```
3. **Database Connection**

View File

@@ -15,10 +15,10 @@ jobs:
- name: Checkout Code 🛎
uses: actions/checkout@v4
- name: Setup Bun 🌟
uses: oven-sh/setup-bun@v1
- name: Setup pnpm 🌟
uses: pnpm/action-setup@v4
with:
bun-version: latest
version: latest
- name: Setup Node 📦
uses: actions/setup-node@v4
@@ -26,4 +26,4 @@ jobs:
node-version: latest
- name: Install dependencies 📦
run: bun install
run: pnpm install

View File

@@ -27,10 +27,10 @@ jobs:
- name: Checkout Code 🛎
uses: actions/checkout@v4
- name: Setup Bun 🌟
uses: oven-sh/setup-bun@v1
- name: Setup pnpm 🌟
uses: pnpm/action-setup@v4
with:
bun-version: latest
version: latest
- name: Setup Node 📦
uses: actions/setup-node@v4
@@ -38,7 +38,7 @@ jobs:
node-version: latest
- name: Install dependencies 📦
run: bun install
run: pnpm install
- name: Run Lingo.dev Localization 🌐
if: ${{ !inputs.skip_localization }}

7
.gitignore vendored
View File

@@ -1,9 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/apps/*/node_modules
/packages/*/node_modules
node_modules
/.pnp
.pnp.*
.yarn/*
@@ -53,4 +51,5 @@ apps/mail/scripts.ts
.wrangler
worker-configuration.d.ts
.dev.vars.*
.dev.vars.*
.react-router

View File

@@ -45,7 +45,7 @@ Zero is built with modern and reliable technologies:
**Required Versions:**
- [Node.js](https://nodejs.org/en/download) (v18 or higher)
- [Bun](https://bun.sh) (v1.2 or higher)
- [pnpm](https://pnpm.io) (v10 or higher)
- [Docker](https://docs.docker.com/engine/install/) (v20 or higher)
Before running the application, you'll need to set up services and configure environment variables. For more details on environment variables, see the [Environment Variables](#environment-variables) section.
@@ -67,10 +67,10 @@ You can set up Zero in two ways:
cd Zero
# Install dependencies
bun install
pnpm install
# Start database locally
bun docker:db:up
pnpm docker:up
```
2. **Set Up Environment**
@@ -80,14 +80,14 @@ You can set up Zero in two ways:
cp .env.example .env
```
- Configure your environment variables (see below)
- Setup cloudflare with `bun run cf-install`, you will need to run this everytime there is a `.env` change
- Start the database with the provided docker compose setup: `bun docker:db:up`
- Initialize the database: `bun db:push`
- Setup cloudflare with `pnpm run cf-install`, you will need to run this everytime there is a `.env` change
- Start the database with the provided docker compose setup: `pnpm docker:up`
- Initialize the database: `pnpm db:push`
3. **Start the App**
```bash
bun dev
pnpm dev
```
4. **Open in Browser**
@@ -95,38 +95,6 @@ You can set up Zero in two ways:
Visit [http://localhost:3000](http://localhost:3000)
</details>
<details>
<summary><b>Option 2: Dev Container Setup (For VS Code Users)</b></summary>
This option uses VS Code's Dev Containers feature to provide a fully configured development environment with all dependencies pre-installed. It's great for ensuring everyone on the team has the same setup.
1. **Prerequisites**
- [Docker](https://docs.docker.com/get-docker/)
- [VS Code](https://code.visualstudio.com/) or compatible editor
- [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
2. **Open in Dev Container**
- Clone the repository: `git clone https://github.com/Mail-0/Zero.git`
- Open the folder in VS Code
- When prompted, click "Reopen in Container" or run the "Dev Containers: Open Folder in Container" command
- VS Code will build and start the dev container (this may take a few minutes the first time)
3. **Access the App**
- The app will be available at [http://localhost:3000](http://localhost:3000)
4. **Troubleshooting**
- If you encounter issues with the container, try rebuilding it using the "Dev Containers: Rebuild Container" command
- For dependency issues inside the container:
`bash
rm -rf node_modules
rm bun.lockb
bun install
`
</details>
### Environment Setup
1. **Better Auth Setup**
@@ -211,7 +179,7 @@ Zero uses PostgreSQL for storing data. Here's how to set it up:
Run this command to start a local PostgreSQL instance:
```bash
bun docker:db:up
pnpm docker:up
```
This creates a database with:
@@ -223,7 +191,7 @@ Zero uses PostgreSQL for storing data. Here's how to set it up:
2. **Set Up Database Connection**
Make sure your database connection string is in `.env` file. And you have ran `bun run cf-install` to sync the latest env.
Make sure your database connection string is in `.env` file. And you have ran `pnpm run cf-install` to sync the latest env.
For local development use:
@@ -236,26 +204,26 @@ Zero uses PostgreSQL for storing data. Here's how to set it up:
- **Set up database tables**:
```bash
bun db:push
pnpm db:push
```
- **Create migration files** (after schema changes):
```bash
bun db:generate
pnpm db:generate
```
- **Apply migrations**:
```bash
bun db:migrate
pnpm db:migrate
```
- **View database content**:
```bash
bun db:studio
pnpm db:studio
```
> If you run `bun dev` in your terminal, the studio command should be automatically running with the app.
> If you run `pnpm dev` in your terminal, the studio command should be automatically running with the app.
## Contribute

View File

@@ -1,7 +1,5 @@
'use client';
import { TriangleAlert } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import { useQueryState } from 'nuqs';
import { useEffect } from 'react';
import { toast } from 'sonner';

View File

@@ -1,5 +1,3 @@
'use client';
import { useEffect, type ReactNode, useState, Suspense } from 'react';
import type { EnvVarInfo } from '@zero/server/auth-providers';
import ErrorMessage from '@/app/(auth)/login/error-message';
@@ -7,9 +5,7 @@ import { signIn, useSession } from '@/lib/auth-client';
import { Google, Microsoft } from '@/components/icons/icons';
import { Button } from '@/components/ui/button';
import { TriangleAlert } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { env } from '@/lib/env';
import Image from 'next/image';
import { useNavigate } from 'react-router';
import { toast } from 'sonner';
interface EnvVarStatus {
@@ -48,14 +44,14 @@ const getProviderIcon = (providerId: string, className?: string): ReactNode => {
case 'zero':
return (
<>
<Image
<img
src="/white-icon.svg"
alt="Zero"
width={15}
height={15}
className="mr-2 hidden dark:block"
/>
<Image
<img
src="/black-icon.svg"
alt="Zero"
width={15}
@@ -70,7 +66,7 @@ const getProviderIcon = (providerId: string, className?: string): ReactNode => {
};
function LoginClientContent({ providers, isProd }: LoginClientProps) {
const router = useRouter();
const navigate = useNavigate();
const [expandedProviders, setExpandedProviders] = useState<Record<string, boolean>>({});
useEffect(() => {
@@ -78,7 +74,7 @@ function LoginClientContent({ providers, isProd }: LoginClientProps) {
if (missing?.id) {
setExpandedProviders({ [missing.id]: true });
}
}, [providers, router]);
}, [providers]);
const missingRequiredProviders = providers
.filter((p) => p.required && !p.enabled)
@@ -110,12 +106,12 @@ function LoginClientContent({ providers, isProd }: LoginClientProps) {
const handleProviderClick = (provider: Provider) => {
if (provider.isCustom && provider.customRedirectPath) {
router.push(provider.customRedirectPath);
navigate(provider.customRedirectPath);
} else {
toast.promise(
signIn.social({
provider: provider.id as any,
callbackURL: `${env.NEXT_PUBLIC_APP_URL}/mail`,
callbackURL: `${window.location.origin}/mail`,
}),
{
error: 'Login redirect failed',

View File

@@ -1,18 +1,10 @@
import { authProviders, customProviders, isProviderEnabled } from '@zero/server/auth-providers';
import { authProxy } from '@/lib/auth-proxy';
import { LoginClient } from './login-client';
import { redirect } from 'next/navigation';
import { headers } from 'next/headers';
import { env } from '@/lib/env';
import { useLoaderData } from 'react-router';
import { env } from 'cloudflare:workers';
export default async function LoginPage() {
const headersList = new Headers(Object.fromEntries(await (await headers()).entries()));
const session = await authProxy.api.getSession({ headers: headersList });
if (session?.connectionId) {
redirect('/mail/inbox');
}
const envNodeEnv = env.NODE_ENV;
const isProd = envNodeEnv === 'production';
export function loader() {
const isProd = !import.meta.env.DEV;
const authProviderStatus = authProviders(env as unknown as Record<string, string>).map(
(provider) => {
@@ -51,6 +43,15 @@ export default async function LoginPage() {
const allProviders = [...customProviderStatus, ...authProviderStatus];
return {
allProviders,
isProd,
};
}
export default function LoginPage() {
const { allProviders, isProd } = useLoaderData<typeof loader>();
return (
<div className="flex min-h-screen w-full flex-col bg-white dark:bg-black">
<LoginClient providers={allProviders} isProd={isProd} />

View File

@@ -1,12 +1,10 @@
'use client';
import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useForm } from 'react-hook-form';
import { Link } from 'react-router';
import { toast } from 'sonner';
import Link from 'next/link';
import { z } from 'zod';
const formSchema = z.object({
@@ -69,7 +67,7 @@ export default function LoginZero() {
<div className="flex items-center justify-between">
<FormLabel className="text-muted-foreground">Password</FormLabel>
<Link
href="/forgot-password"
to="/forgot-password"
className="text-muted-foreground text-xs hover:text-white"
>
Forgot your password?

View File

@@ -1,12 +1,9 @@
'use client';
import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import Link from 'next/link';
import { z } from 'zod';
const formSchema = z.object({

View File

@@ -1,44 +0,0 @@
'use client';
import { AlertCircle, ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
export function NotFound() {
const router = useRouter();
const t = useTranslations();
return (
<div className="dark:bg-background flex w-full items-center justify-center bg-white text-center">
<div className="flex-col items-center justify-center md:flex dark:text-gray-100">
<div className="relative">
<h1 className="text-muted-foreground/20 select-none text-[150px] font-bold">404</h1>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<AlertCircle className="text-muted-foreground h-20 w-20" />
</div>
</div>
{/* Message */}
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">
{t('pages.error.notFound.title')}
</h2>
<p className="text-muted-foreground">{t('pages.error.notFound.description')}</p>
</div>
{/* Buttons */}
<div className="mt-2 flex gap-2">
<Button
variant="outline"
onClick={() => router.back()}
className="text-muted-foreground gap-2"
>
<ArrowLeft className="h-4 w-4" />
{t('pages.error.notFound.goBack')}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -1,5 +1,3 @@
'use client';
import { Card, CardHeader, CardTitle } from '@/components/ui/card';
import { Github, Mail, ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';

View File

@@ -1,5 +1,3 @@
'use client';
import {
Github,
Star,
@@ -30,8 +28,6 @@ import { useEffect, useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import Image from 'next/image';
import Link from 'next/link';
interface Contributor {
login: string;
@@ -381,16 +377,14 @@ export default function OpenPage() {
<div className="flex items-center gap-2">
<a href="/">
<div className="relative h-8 w-8">
<Image
<img
src="/black-icon.svg"
alt="0.email Logo"
fill
className="object-contain dark:hidden"
/>
<Image
<img
src="/white-icon.svg"
alt="0.email Logo"
fill
className="hidden object-contain dark:block"
/>
</div>
@@ -696,24 +690,24 @@ export default function OpenPage() {
{specialRoles[member.login.toLowerCase()]?.role || 'Maintainer'}
</p>
<div className="mt-3 flex gap-2">
<Link
<a
href={`https://github.com/${member.login}`}
target="_blank"
className="rounded-md p-1 text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-white"
>
<Github className="h-4 w-4" />
</Link>
</a>
{specialRoles[member.login.toLowerCase()]?.x && (
<Link
<a
href={`https://x.com/${specialRoles[member.login.toLowerCase()]?.x}`}
target="_blank"
className="rounded-md p-1 text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-white"
>
<Twitter className="h-4 w-4" />
</Link>
</a>
)}
{specialRoles[member.login.toLowerCase()]?.website && (
<Link
<a
href={specialRoles[member.login.toLowerCase()]?.website || '#'}
target="_blank"
className="rounded-md p-1 text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-white"
@@ -732,7 +726,7 @@ export default function OpenPage() {
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"
/>
</svg>
</Link>
</a>
)}
</div>
</div>
@@ -789,7 +783,7 @@ export default function OpenPage() {
<TabsContent value="grid">
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
{filteredContributors?.map((contributor, index) => (
<Link
<a
key={contributor.login}
href={contributor.html_url}
target="_blank"
@@ -823,7 +817,7 @@ export default function OpenPage() {
</span>
</div>
</div>
</Link>
</a>
))}
</div>
</TabsContent>
@@ -940,7 +934,7 @@ export default function OpenPage() {
asChild
className="relative overflow-hidden bg-neutral-900 text-white transition-all hover:bg-neutral-800 dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-100"
>
<Link
<a
href={`https://github.com/${REPOSITORY}/blob/main/.github/CONTRIBUTING.md`}
target="_blank"
>
@@ -948,7 +942,7 @@ export default function OpenPage() {
<GitGraph className="mr-2 h-4 w-4" />
Start Contributing
</span>
</Link>
</a>
</Button>
<Button
asChild
@@ -1020,22 +1014,22 @@ export default function OpenPage() {
</div>
<div className="mb-6 mt-2 flex items-center justify-center gap-4">
<Link
<a
href="https://discord.gg/BCFr6FFt"
target="_blank"
className="text-neutral-500 transition-colors hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200"
aria-label="Join our Discord"
>
<Discord className="h-4 w-4" />
</Link>
<Link
</a>
<a
href="https://x.com/zerodotemail"
target="_blank"
className="text-neutral-500 transition-colors hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200"
aria-label="Follow us on X (Twitter)"
>
<Twitter className="h-4 w-4" />
</Link>
</a>
</div>
</div>
</div>

View File

@@ -1,4 +1,5 @@
'use client';
export default function FullWidthLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
import { Outlet } from 'react-router';
export default function FullWidthLayout() {
return <Outlet />;
}

View File

@@ -1,5 +1,3 @@
'use client';
import {
NavigationMenu,
NavigationMenuItem,
@@ -11,22 +9,17 @@ import {
} from '@/components/ui/navigation-menu';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
import { PixelatedBackground } from '@/components/home/pixelated-bg';
import { CircleCheck, CircleX } from '@/components/icons/icons';
import PricingCard from '@/components/pricing/pricing-card';
import Comparision from '@/components/pricing/comparision';
import { TextShimmer } from '@/components/ui/text-shimmer';
import { signIn, useSession } from '@/lib/auth-client';
import { Separator } from '@/components/ui/separator';
import { useBilling } from '@/hooks/use-billing';
import { Link, useNavigate } from 'react-router';
import { Button } from '@/components/ui/button';
import Footer from '@/components/home/footer';
import { useCustomer } from 'autumn-js/next';
import { useRouter } from 'next/navigation';
import { useState, useMemo } from 'react';
import { Menu } from 'lucide-react';
import Image from 'next/image';
import { toast } from 'sonner';
import Link from 'next/link';
const resources = [
{
@@ -74,7 +67,7 @@ const aboutLinks = [
];
export default function PricingPage() {
const router = useRouter();
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const { data: session } = useSession();
@@ -92,12 +85,12 @@ export default function PricingPage() {
<header className="fixed z-50 hidden w-full items-center justify-center px-4 pt-6 md:flex">
<nav className="border-input/50 relative z-50 flex w-full max-w-3xl items-center justify-between gap-2 rounded-xl border-t bg-[#1E1E1E] p-2 px-4">
<div className="flex items-center gap-6">
<a href="/" className="relative bottom-1 cursor-pointer">
<Image src="white-icon.svg" alt="Zero Email" width={22} height={22} />
<Link to="/" className="relative bottom-1 cursor-pointer">
<img src="white-icon.svg" alt="Zero Email" width={22} height={22} />
<span className="text-muted-foreground absolute -right-[-0.5px] text-[10px]">
beta
</span>
</a>
</Link>
<NavigationMenu>
<NavigationMenuList className="gap-1">
<NavigationMenuItem>
@@ -146,13 +139,13 @@ export default function PricingPage() {
onClick={() => {
if (session) {
// User is logged in, redirect to inbox
router.push('/mail/inbox');
navigate('/mail/inbox');
} else {
// User is not logged in, show sign-in dialog
toast.promise(
signIn.social({
provider: 'google',
callbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/mail`,
callbackURL: `${window.location.origin}/mail`,
}),
{
error: 'Login redirect failed',
@@ -182,7 +175,7 @@ export default function PricingPage() {
<SheetContent side="left" className="w-[300px] bg-[#111111] sm:w-[400px]">
<SheetHeader className="flex flex-row items-center justify-between">
<SheetTitle>
<Image src="white-icon.svg" alt="Zero Email" width={22} height={22} />
<img src="white-icon.svg" alt="Zero Email" width={22} height={22} />
</SheetTitle>
<a href="/login">
<Button className="w-full">Sign in</Button>
@@ -206,11 +199,11 @@ export default function PricingPage() {
{resources.map((resource) => (
<Link
key={resource.title}
href={resource.href}
to={resource.href}
className="flex items-center gap-2 font-medium"
>
{resource.platform && (
<Image
<img
src={`/${resource.platform}.svg`}
alt={resource.platform}
width={20}

View File

@@ -1,19 +1,17 @@
'use client';
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard';
import { Card, CardHeader, CardTitle } from '@/components/ui/card';
import { Github, Mail, ArrowLeft, Link2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import Footer from '@/components/home/footer';
import { createSectionId } from '@/lib/utils';
import { useRouter } from 'next/navigation';
import { useNavigate } from 'react-router';
import { toast } from 'sonner';
import React from 'react';
const LAST_UPDATED = 'May 16, 2025';
export default function PrivacyPolicy() {
const router = useRouter();
const navigate = useNavigate();
const { copiedValue: copiedSection, copyToClipboard } = useCopyToClipboard();
const handleCopyLink = (sectionId: string) => {

View File

@@ -1,12 +1,10 @@
'use client';
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard';
import { Card, CardHeader, CardTitle } from '@/components/ui/card';
import { Github, ArrowLeft, Link2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import Footer from '@/components/home/footer';
import { createSectionId } from '@/lib/utils';
import { useRouter } from 'next/navigation';
import { useNavigate } from 'react-router';
import { toast } from 'sonner';
import React from 'react';
@@ -14,6 +12,7 @@ const LAST_UPDATED = 'February 13, 2025';
export default function TermsOfService() {
const { copiedValue: copiedSection, copyToClipboard } = useCopyToClipboard();
const navigate = useNavigate();
const handleCopyLink = (sectionId: string) => {
const url = `${window.location.origin}${window.location.pathname}#${sectionId}`;

View File

@@ -1,18 +0,0 @@
import { NextResponse } from "next/server";
export function GET() {
return new NextResponse(
JSON.stringify({
associatedApplications: [
{
applicationId: "ecf043a0-41bb-4c89-bd31-7e3f272f8e3c",
},
],
}),
{
headers: {
"Content-Type": "application/json",
},
},
);
}

View File

@@ -1,13 +0,0 @@
import { NextResponse } from "next/server";
export function GET() {
return new NextResponse(
JSON.stringify({
associatedApplications: [
{
applicationId: "80b11343-e52c-4969-81e8-faebfed78a67",
},
],
}),
);
}

View File

@@ -1,10 +1,7 @@
'use client';
import { Github, Book, Users, Terminal, Code2, Webhook, ArrowRight, ArrowLeft } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useNavigate } from 'react-router';
const developerResources = [
{
@@ -70,7 +67,7 @@ const developerResources = [
] as const;
export default function DeveloperPage() {
const router = useRouter();
const navigate = useNavigate();
return (
<div className="bg-background flex min-h-screen w-full flex-col">
@@ -80,7 +77,7 @@ export default function DeveloperPage() {
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
onClick={() => navigate(-1)}
className="text-muted-foreground hover:text-foreground mb-6 gap-2"
>
<ArrowLeft className="h-4 w-4" />

View File

@@ -1,13 +1,14 @@
'use client';
import { HotkeyProviderWrapper } from '@/components/providers/hotkey-provider-wrapper';
import { CommandPaletteProvider } from '@/components/context/command-palette-context';
import { Outlet } from 'react-router';
export default function Layout({ children }: { children: React.ReactNode }) {
export default function Layout() {
return (
<HotkeyProviderWrapper>
<CommandPaletteProvider>
<div className="relative flex max-h-screen w-full overflow-hidden">{children}</div>
<div className="relative flex max-h-screen w-full overflow-hidden">
<Outlet />
</div>
</CommandPaletteProvider>
</HotkeyProviderWrapper>
);

View File

@@ -1,32 +1,21 @@
import { MailLayout } from '@/components/mail/mail';
import { authProxy } from '@/lib/auth-proxy';
import { redirect } from 'next/navigation';
import { headers } from 'next/headers';
interface MailPageProps {
params: Promise<{
folder: string;
}>;
searchParams: Promise<{
threadId: string;
}>;
}
import { useLoaderData } from 'react-router';
import type { Route } from './+types/page';
const ALLOWED_FOLDERS = ['inbox', 'draft', 'sent', 'spam', 'bin', 'archive'];
export default async function MailPage({ params }: MailPageProps) {
const headersList = new Headers(Object.fromEntries(await (await headers()).entries()));
const session = await authProxy.api.getSession({ headers: headersList });
export async function loader({ params, request }: Route.LoaderArgs) {
const session = await authProxy.api.getSession({ headers: request.headers });
if (!session) return Response.redirect(`${import.meta.env.VITE_PUBLIC_APP_URL}/login`);
if (!session?.user.id) {
redirect('/login');
}
const { folder } = await params;
if (!ALLOWED_FOLDERS.includes(folder)) {
return <div>Invalid folder</div>;
}
return {
folder: params.folder,
};
}
export default function MailPage() {
const { folder } = useLoaderData<typeof loader>();
if (!ALLOWED_FOLDERS.includes(folder)) return <div>Invalid folder</div>;
return <MailLayout />;
}

View File

@@ -7,40 +7,32 @@ import {
} from '@/components/ui/dialog';
import { CreateEmail } from '@/components/create/create-email';
import { authProxy } from '@/lib/auth-proxy';
import { redirect } from 'next/navigation';
import { headers } from 'next/headers';
import { useLoaderData } from 'react-router';
import type { Route } from './+types/page';
// Define the type for search params
interface ComposePageProps {
searchParams: Promise<{
export async function loader({ request }: Route.LoaderArgs) {
const session = await authProxy.api.getSession({ headers: request.headers });
if (!session) return Response.redirect(`${import.meta.env.VITE_PUBLIC_APP_URL}/login`);
const url = new URL(request.url);
if (url.searchParams.get('to')?.startsWith('mailto:')) {
return Response.redirect(
`${import.meta.env.VITE_PUBLIC_APP_URL}/mail/compose/handle-mailto?mailto=${encodeURIComponent(url.searchParams.get('to') ?? '')}`,
);
}
return Object.fromEntries(url.searchParams.entries()) as {
to?: string;
subject?: string;
body?: string;
draftId?: string;
cc?: string;
bcc?: string;
}>;
};
}
export default async function ComposePage({ searchParams }: ComposePageProps) {
const headersList = new Headers(Object.fromEntries(await (await headers()).entries()));
const session = await authProxy.api.getSession({ headers: headersList });
export default function ComposePage() {
const params = useLoaderData<typeof loader>();
if (!session?.user.id) {
redirect('/login');
}
// Need to await searchParams in Next.js 15+
const params = await searchParams;
// Check if this is a mailto URL
const toParam = params.to || '';
if (toParam.startsWith('mailto:')) {
// Redirect to our dedicated mailto handler
redirect(`/mail/compose/handle-mailto?mailto=${encodeURIComponent(toParam)}`);
}
// Handle normal compose page (direct or with draftId)
return (
<Dialog open={true}>
<DialogTitle></DialogTitle>

View File

@@ -1,55 +1,47 @@
import { CreateEmail } from '@/components/create/create-email';
import { authProxy } from '@/lib/auth-proxy';
import { redirect } from 'next/navigation';
import { headers } from 'next/headers';
import type { Route } from './+types/page';
// Define the type for search params
interface CreatePageProps {
searchParams: Promise<{
export async function loader({ request }: Route.LoaderArgs) {
const session = await authProxy.api.getSession({ headers: request.headers });
if (!session) return Response.redirect(`${import.meta.env.VITE_PUBLIC_APP_URL}/login`);
const url = new URL(request.url);
const params = Object.fromEntries(url.searchParams.entries()) as {
to?: string;
subject?: string;
body?: string;
}>;
}
export default async function CreatePage({ searchParams }: CreatePageProps) {
const headersList = new Headers(Object.fromEntries(await (await headers()).entries()));
const session = await authProxy.api.getSession({ headers: headersList });
if (!session?.user.id) {
redirect('/login');
}
const params = await searchParams;
};
const toParam = params.to || 'someone@someone.com';
redirect(
`/mail/inbox?isComposeOpen=true&to=${encodeURIComponent(toParam)}${params.subject ? `&subject=${encodeURIComponent(params.subject)}` : ''}`,
return Response.redirect(
`${import.meta.env.VITE_PUBLIC_APP_URL}/mail/inbox?isComposeOpen=true&to=${encodeURIComponent(toParam)}${params.subject ? `&subject=${encodeURIComponent(params.subject)}` : ''}`,
);
}
export async function generateMetadata({ searchParams }: CreatePageProps) {
// Need to await searchParams in Next.js 15+
const params = await searchParams;
// export async function generateMetadata({ searchParams }: any) {
// // Need to await searchParams in Next.js 15+
// const params = await searchParams;
const toParam = params.to || 'someone';
// const toParam = params.to || 'someone';
// Create common metadata properties
const title = `Email ${toParam} on Zero`;
const description = 'Zero - The future of email is here';
const imageUrl = `/og-api/create?to=${encodeURIComponent(toParam)}${params.subject ? `&subject=${encodeURIComponent(params.subject)}` : ''}`;
// // Create common metadata properties
// const title = `Email ${toParam} on Zero`;
// const description = 'Zero - The future of email is here';
// const imageUrl = `/og-api/create?to=${encodeURIComponent(toParam)}${params.subject ? `&subject=${encodeURIComponent(params.subject)}` : ''}`;
// Create metadata object
return {
title,
description,
openGraph: {
title,
description,
images: [imageUrl],
},
twitter: {
card: 'summary_large_image',
title,
description,
images: [imageUrl],
},
};
}
// // Create metadata object
// return {
// title,
// description,
// openGraph: {
// title,
// description,
// images: [imageUrl],
// },
// twitter: {
// card: 'summary_large_image',
// title,
// description,
// images: [imageUrl],
// },
// };
// }

View File

@@ -2,16 +2,25 @@ import { HotkeyProviderWrapper } from '@/components/providers/hotkey-provider-wr
import { OnboardingWrapper } from '@/components/onboarding';
import { NotificationProvider } from '@/components/party';
import { AppSidebar } from '@/components/ui/app-sidebar';
import { headers } from 'next/headers';
export default async function MailLayout({ children }: { children: React.ReactNode }) {
const headersList = await headers();
import { Outlet, useLoaderData } from 'react-router';
import type { Route } from './+types/layout';
export async function loader({ request }: Route.LoaderArgs) {
return {
headers: Object.fromEntries(request.headers.entries()),
};
}
export default function MailLayout() {
const { headers } = useLoaderData<typeof loader>();
return (
<HotkeyProviderWrapper>
<AppSidebar />
<div className="bg-lightBackground dark:bg-darkBackground w-full">{children}</div>
<div className="bg-lightBackground dark:bg-darkBackground w-full">
<Outlet />
</div>
<OnboardingWrapper />
<NotificationProvider headers={Object.fromEntries(headersList.entries())} />
<NotificationProvider headers={headers} />
</HotkeyProviderWrapper>
);
}

View File

@@ -0,0 +1,5 @@
import { redirect } from 'react-router';
export function loader() {
throw redirect(`/mail/inbox`);
}

View File

@@ -1,5 +1,3 @@
'use client';
import { Button } from '@/components/ui/button';
import { ArrowLeft } from 'lucide-react';

View File

@@ -1,5 +1,3 @@
'use client';
import { SidebarToggle } from '@/components/ui/sidebar-toggle';
import { Construction } from 'lucide-react';
import BackButton from './back-button';

View File

@@ -1,13 +1,11 @@
'use client';
import NotificationsPage from '../notifications/page';
import ConnectionsPage from '../connections/page';
import AppearancePage from '../appearance/page';
import ShortcutsPage from '../shortcuts/page';
import SecurityPage from '../security/page';
import { useParams } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import GeneralPage from '../general/page';
import { useParams } from 'react-router';
import LabelsPage from '../labels/page';
const settingsPages: Record<string, React.ComponentType> = {

View File

@@ -1,5 +1,3 @@
'use client';
import {
Form,
FormControl,
@@ -23,7 +21,7 @@ import { useMutation } from '@tanstack/react-query';
import { useSettings } from '@/hooks/use-settings';
import { Laptop, Moon, Sun } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import { useForm } from 'react-hook-form';
import { useTheme } from 'next-themes';
import { useState } from 'react';

View File

@@ -1,5 +1,3 @@
'use client';
import {
Dialog,
DialogContent,
@@ -22,9 +20,8 @@ import { useThreads } from '@/hooks/use-threads';
import { emailProviders } from '@/lib/constants';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import { useState } from 'react';
import Image from 'next/image';
import { toast } from 'sonner';
export default function ConnectionsPage() {
@@ -90,7 +87,7 @@ export default function ConnectionsPage() {
>
<div className="flex min-w-0 items-center gap-4">
{connection.picture ? (
<Image
<img
src={connection.picture}
alt=""
className="h-12 w-12 shrink-0 rounded-lg object-cover"

View File

@@ -1,5 +1,3 @@
'use client';
import {
Dialog,
DialogContent,
@@ -24,8 +22,8 @@ import { Button } from '@/components/ui/button';
import { useSession } from '@/lib/auth-client';
import { Input } from '@/components/ui/input';
import { AlertTriangle } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { useNavigate } from 'react-router';
import { useTranslations } from 'use-intl';
import { useForm } from 'react-hook-form';
import { useState } from 'react';
import { toast } from 'sonner';
@@ -42,7 +40,7 @@ const formSchema = z.object({
function DeleteAccountDialog() {
const [isOpen, setIsOpen] = useState(false);
const t = useTranslations();
const router = useRouter();
const navigate = useNavigate();
const trpc = useTRPC();
const { refetch } = useSession();
const { mutateAsync: deleteAccount, isPending } = useMutation(trpc.user.delete.mutationOptions());
@@ -63,7 +61,7 @@ function DeleteAccountDialog() {
if (!success) return toast.error(message);
refetch();
toast.success('Account deleted successfully');
router.push('/');
navigate('/');
setIsOpen(false);
},
onError: (error) => {

View File

@@ -1,5 +1,3 @@
'use client';
import {
Form,
FormControl,
@@ -16,23 +14,23 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { availableLocales, locales, type Locale } from '@/i18n/config';
import { useForm, type ControllerRenderProps } from 'react-hook-form';
import { userSettingsSchema } from '@zero/db/user_settings_default';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { SettingsCard } from '@/components/settings/settings-card';
import { useState, useEffect, useMemo, memo } from 'react';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useTranslations, useLocale } from 'next-intl';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations, useLocale } from 'use-intl';
import { useTRPC } from '@/providers/query-provider';
import { getBrowserTimezone } from '@/lib/timezones';
import { Textarea } from '@/components/ui/textarea';
import { useSettings } from '@/hooks/use-settings';
import { Globe, Clock, XIcon } from 'lucide-react';
import { availableLocales } from '@/i18n/config';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { changeLocale } from '@/i18n/utils';
import { useRevalidator } from 'react-router';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import * as z from 'zod';
@@ -123,6 +121,10 @@ export default function GeneralPage() {
const trpc = useTRPC();
const queryClient = useQueryClient();
const { mutateAsync: saveUserSettings } = useMutation(trpc.settings.save.mutationOptions());
const { mutateAsync: setLocaleCookie } = useMutation(
trpc.cookiePreferences.setLocaleCookie.mutationOptions(),
);
const { revalidate } = useRevalidator();
const form = useForm<z.infer<typeof userSettingsSchema>>({
resolver: zodResolver(userSettingsSchema),
@@ -149,13 +151,13 @@ export default function GeneralPage() {
if (!updater) return;
return { settings: { ...updater.settings, ...values } };
});
if (values.language !== locale) {
await changeLocale(values.language as Locale);
const localeName = new Intl.DisplayNames([values.language], { type: 'language' }).of(
values.language,
);
toast.success(t('common.settings.languageChanged', { locale: localeName }));
}
await setLocaleCookie({ locale: values.language });
const localeName = new Intl.DisplayNames([values.language], { type: 'language' }).of(
values.language,
);
toast.success(t('common.settings.languageChanged', { locale: localeName! }));
await revalidate();
toast.success(t('common.settings.saved'));
} catch (error) {

View File

@@ -1,5 +1,3 @@
'use client';
import {
Dialog,
DialogContent,
@@ -32,7 +30,7 @@ import { GMAIL_COLORS } from '@/lib/constants';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import { useForm } from 'react-hook-form';
import { Command } from 'lucide-react';
import { COLORS } from './colors';

View File

@@ -1,6 +1,10 @@
'use client';
import { SettingsLayoutContent } from '@/components/ui/settings-content';
import { Outlet } from 'react-router';
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
return <SettingsLayoutContent>{children}</SettingsLayoutContent>;
export default function SettingsLayout() {
return (
<SettingsLayoutContent>
<Outlet />
</SettingsLayoutContent>
);
}

View File

@@ -1,5 +1,3 @@
'use client';
import {
Form,
FormControl,

View File

@@ -0,0 +1,5 @@
import { redirect } from 'react-router';
export function loader() {
throw redirect(`/settings/general`);
}

View File

@@ -1,5 +1,3 @@
'use client';
import {
Form,
FormControl,
@@ -19,8 +17,8 @@ import { useMutation } from '@tanstack/react-query';
import { useSettings } from '@/hooks/use-settings';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { useTranslations } from 'next-intl';
import { useState, useEffect } from 'react';
import { useTranslations } from 'use-intl';
import { useForm } from 'react-hook-form';
import { XIcon } from 'lucide-react';
import { toast } from 'sonner';

View File

@@ -1,5 +1,3 @@
'use client';
import {
Form,
FormControl,
@@ -12,7 +10,7 @@ import { SettingsCard } from '@/components/settings/settings-card';
import { zodResolver } from '@hookform/resolvers/zod';
import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import { useForm } from 'react-hook-form';
import { KeyRound } from 'lucide-react';
import { useState } from 'react';
@@ -61,12 +59,12 @@ export default function SecurityPage() {
>
<Form {...form}>
<form id="security-form" onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<div className="flex flex-col md:flex-row w-full items-center gap-5">
<div className="flex w-full flex-col items-center gap-5 md:flex-row">
<FormField
control={form.control}
name="twoFactorAuth"
render={({ field }) => (
<FormItem className="bg-popover flex flex-row items-center justify-between rounded-lg border p-4 w-full md:w-auto">
<FormItem className="bg-popover flex w-full flex-row items-center justify-between rounded-lg border p-4 md:w-auto">
<div className="space-y-0.5">
<FormLabel className="text-base">
{t('pages.settings.security.twoFactorAuth')}
@@ -85,7 +83,7 @@ export default function SecurityPage() {
control={form.control}
name="loginNotifications"
render={({ field }) => (
<FormItem className="bg-popover flex flex-row items-center justify-between rounded-lg border p-4 w-full md:w-auto">
<FormItem className="bg-popover flex w-full flex-row items-center justify-between rounded-lg border p-4 md:w-auto">
<div className="space-y-0.5">
<FormLabel className="text-base">
{t('pages.settings.security.loginNotifications')}

View File

@@ -1,7 +1,7 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import type { MessageKey } from '@/config/navigation';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { useTranslations } from 'use-intl';
interface HotkeyRecorderProps {
isOpen: boolean;

View File

@@ -1,5 +1,3 @@
'use client';
import { keyboardShortcuts, type Shortcut } from '@/config/shortcuts';
import { SettingsCard } from '@/components/settings/settings-card';
import { formatDisplayKeys } from '@/lib/hotkeys/use-hotkey-utils';
@@ -9,7 +7,7 @@ import type { MessageKey } from '@/config/navigation';
import { HotkeyRecorder } from './hotkey-recorder';
import { Button } from '@/components/ui/button';
import { useSession } from '@/lib/auth-client';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import { toast } from 'sonner';
export default function ShortcutsPage() {

View File

@@ -1,5 +1,5 @@
// 'use client';
// // DEPRECATED -
//
// // DEPRECATED -
// import {
// Form,
// FormControl,
@@ -10,7 +10,7 @@
// } from '@/components/ui/form';
// import { SettingsCard } from '@/components/settings/settings-card';
// import { useState, useEffect, useRef } from 'react';
// import { useTranslations } from 'next-intl';
// import { useTranslations } from 'use-intl';
// import { zodResolver } from '@hookform/resolvers/zod';
// import { saveUserSettings } from '@/actions/settings';
// import { useSettings } from '@/hooks/use-settings';
@@ -63,12 +63,12 @@
// // Create a temporary div to parse the HTML
// const div = document.createElement('div');
// div.innerHTML = html;
// // Return as a document with proper structure preserving paragraphs
// // This is a basic implementation - for more complex conversions, consider using a
// // This is a basic implementation - for more complex conversions, consider using a
// // dedicated HTML-to-ProseMirror conversion library
// const content: any[] = [];
// // Process each child element to create proper paragraph nodes
// Array.from(div.childNodes).forEach(node => {
// if (node.nodeType === Node.TEXT_NODE) {
@@ -96,12 +96,12 @@
// });
// }
// });
// // If no content was created, create a default empty paragraph
// if (content.length === 0) {
// content.push({ type: 'paragraph' });
// }
// return {
// type: 'doc',
// content
@@ -122,11 +122,11 @@
// editorType: settings.signature.editorType || 'plain',
// },
// });
// // Set the raw HTML in the state
// const signatureHtml = settings.signature.content || '--<br><br>Sent via <a href="https://0.email" target="_blank" style="color: #016FFE; text-decoration: none;">0.email</a>';
// setSignatureHtml(signatureHtml);
// // Attempt to parse HTML to JSONContent for the rich editor
// // This is a simple approach - a more robust solution would use a proper HTML to ProseMirror converter
// setEditorContent(tryParseHtmlToContent(signatureHtml));
@@ -140,10 +140,10 @@
// async function onSubmit(values: z.infer<typeof formSchema>) {
// setIsSaving(true);
// // Get the content based on editor type
// let contentToSave = signatureHtml;
// // Sanitize HTML before saving
// const sanitizedHtml = DOMPurify.sanitize(contentToSave, {
// ADD_ATTR: ['target'],
@@ -185,7 +185,7 @@
// // Try to clean any malformed HTML
// const cleanedValue = decodeHtmlEntities(newValue);
// setSignatureHtml(cleanedValue);
// // Update the form state
// form.setValue('signature.content', cleanedValue);
// };
@@ -193,21 +193,21 @@
// const handleEditorChange = (html: string) => {
// // Process the HTML coming from the rich editor
// setSignatureHtml(html);
// // Update the form state
// form.setValue('signature.content', html);
// };
// const watchSignatureEnabled = form.watch('signature.enabled');
// const watchEditorType = form.watch('signature.editorType');
// // Function to decode HTML entities
// const decodeHtmlEntities = (html: string): string => {
// const textarea = document.createElement('textarea');
// textarea.innerHTML = html;
// return textarea.value;
// };
// // Handle switching between editor types
// useEffect(() => {
// // When switching editor modes
@@ -321,7 +321,7 @@
// {/* Signature Editor - either plain text or rich editor */}
// <div className="space-y-2">
// <label className="text-sm font-medium">{t('pages.settings.signatures.signatureContent')}</label>
// {watchEditorType === 'plain' ? (
// <div className="mt-1">
// <Textarea
@@ -335,10 +335,10 @@
// {t('pages.settings.signatures.signatureContentHelp') || "You can use HTML to add formatting, links, and images to your signature."}
// </p>
// <div className="text-xs bg-muted/50 p-2 mt-1 rounded border">
// <strong>Note:</strong> HTML tags are supported for formatting.
// For security reasons, script tags are not allowed. Common useful tags:
// <code className="mx-1 px-1 bg-muted rounded">&lt;a&gt;</code> for links,
// <code className="mx-1 px-1 bg-muted rounded">&lt;br&gt;</code> for line breaks,
// <strong>Note:</strong> HTML tags are supported for formatting.
// For security reasons, script tags are not allowed. Common useful tags:
// <code className="mx-1 px-1 bg-muted rounded">&lt;a&gt;</code> for links,
// <code className="mx-1 px-1 bg-muted rounded">&lt;br&gt;</code> for line breaks,
// <code className="mx-1 px-1 bg-muted rounded">&lt;b&gt;</code> for bold text.
// </div>
// </div>
@@ -383,4 +383,4 @@
// </SettingsCard>
// </div>
// );
// }
// }

View File

@@ -0,0 +1,24 @@
import '../instrument';
import { startTransition, StrictMode } from 'react';
import { HydratedRouter } from 'react-router/dom';
import { hydrateRoot } from 'react-dom/client';
import * as Sentry from '@sentry/react';
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>,
{
onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => {
console.warn('Uncaught error', error, errorInfo.componentStack);
}),
// Callback called when React catches an error in an ErrorBoundary.
onCaughtError: Sentry.reactErrorHandler(),
// Callback called when React automatically recovers from errors.
onRecoverableError: Sentry.reactErrorHandler(),
},
);
});

View File

@@ -0,0 +1,43 @@
import type { AppLoadContext, EntryContext } from 'react-router';
import { renderToReadableStream } from 'react-dom/server';
import { ServerRouter } from 'react-router';
import { isbot } from 'isbot';
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
_loadContext: AppLoadContext,
) {
let shellRendered = false;
const userAgent = request.headers.get('user-agent');
const body = await renderToReadableStream(
<ServerRouter context={routerContext} url={request.url} />,
{
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);
shellRendered = true;
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {
await body.allReady;
}
responseHeaders.set('Content-Type', 'text/html');
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}

View File

@@ -4,7 +4,6 @@ import { Button } from '@/components/ui/button';
import { signOut } from '@/lib/auth-client';
import * as Sentry from '@sentry/nextjs';
import { useEffect } from 'react';
import Error from 'next/error';
export default function ErrorPage({ error, reset }: { error: Error; reset: () => void }) {
useEffect(() => {

View File

@@ -1,16 +0,0 @@
'use client';
import NextError from 'next/error';
export default function GlobalError() {
return (
<html>
<body>
{/* `NextError` is the default Next.js error page component. Its type
definition requires a `statusCode` prop. However, since the App Router
does not expose status codes for errors, we simply pass 0 to render a
generic error message. */}
<NextError statusCode={0} />
</body>
</html>
);
}

View File

@@ -1,21 +1,10 @@
@import '@fontsource-variable/geist';
@import '@fontsource-variable/geist-mono';
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
@@ -24,6 +13,8 @@
@layer base {
:root {
--font-geist-sans: 'Geist Variable', sans-serif;
--font-geist-mono: 'Geist Mono Variable', monospace;
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
@@ -103,7 +94,7 @@
}
body {
@apply bg-background font-sans text-foreground;
@apply bg-background text-foreground font-sans;
}
}
@@ -112,9 +103,9 @@
pointer-events: none !important;
}
*[class^="text-"] {
*[class^='text-'] {
color: transparent;
@apply animate-pulse select-none rounded-md bg-foreground/20;
@apply bg-foreground/20 animate-pulse select-none rounded-md;
}
.skeleton-bg {
@@ -122,7 +113,7 @@
}
.skeleton-div {
@apply animate-pulse bg-foreground/20;
@apply bg-foreground/20 animate-pulse;
}
}
@@ -166,8 +157,8 @@
@apply bg-blue-200 hover:bg-blue-300 dark:bg-blue-500/40 dark:text-blue-50 dark:hover:bg-blue-400/50;
}
.dark [data-hide-on-theme="dark"],
.light [data-hide-on-theme="light"] {
.dark [data-hide-on-theme='dark'],
.light [data-hide-on-theme='light'] {
display: none;
}
@@ -202,26 +193,45 @@
}
.compose-loading {
background: #016FFE;
background: #016ffe;
animation: none;
}
.compose-gradient-animated {
background: linear-gradient(90deg, rgba(255, 213, 208, 1), rgba(219, 255, 228, 1), rgba(226, 214, 255, 1), rgba(255, 213, 208, 1));
background: linear-gradient(
90deg,
rgba(255, 213, 208, 1),
rgba(219, 255, 228, 1),
rgba(226, 214, 255, 1),
rgba(255, 213, 208, 1)
);
background-size: 300% 100%;
animation: gradient-animation 8s ease infinite;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.1),
0 8px 10px -6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
@keyframes gradient-animation {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.compose-gradient-text {
background: linear-gradient(90deg, rgba(255, 213, 208, 1) 0%, rgba(219, 255, 228, 1) 50%, rgba(226, 214, 255, 1) 100%);
background: linear-gradient(
90deg,
rgba(255, 213, 208, 1) 0%,
rgba(219, 255, 228, 1) 50%,
rgba(226, 214, 255, 1) 100%
);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
@@ -245,7 +255,7 @@
}
.compose-gradient-text-shiny::after {
content: "";
content: '';
position: absolute;
top: 0;
left: 0;
@@ -270,12 +280,12 @@
/* Hide scrollbar but keep functionality */
.hide-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.hide-scrollbar::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
display: none; /* Chrome, Safari and Opera */
}
/* remove outline */
@@ -309,7 +319,8 @@
}
@keyframes blink {
0%, 100% {
0%,
100% {
opacity: 1;
}
50% {

View File

@@ -1,47 +0,0 @@
import { ClientProviders } from '@/providers/client-providers';
import { ServerProviders } from '@/providers/server-providers';
import { SpeedInsights } from '@vercel/speed-insights/next';
import { Geist, Geist_Mono } from 'next/font/google';
import { PublicEnvScript } from 'next-runtime-env';
import { siteConfig } from '@/lib/site-config';
import type { PropsWithChildren } from 'react';
import type { Viewport } from 'next';
import { cn } from '@/lib/utils';
import Script from 'next/script';
import './globals.css';
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
});
export { siteConfig as metadata };
export const viewport: Viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#FFFFFF' },
{ media: '(prefers-color-scheme: dark)', color: '#1A1A1A' },
],
};
export default async function RootLayout({ children }: PropsWithChildren) {
return (
<html suppressHydrationWarning>
<head>
<Script src="https://unpkg.com/web-streams-polyfill/dist/polyfill.js" />
<PublicEnvScript />
</head>
<body className={cn(geistSans.variable, geistMono.variable, 'antialiased')}>
<ServerProviders>
<ClientProviders>{children}</ClientProviders>
</ServerProviders>
<SpeedInsights />
</body>
</html>
);
}

View File

@@ -1,7 +1,7 @@
import { cleanEmailAddresses } from '../lib/email-utils';
import { getContext } from 'hono/context-storage';
import type { HonoContext } from '../ctx';
import { serverTrpc } from '../trpc';
import { trpcClient } from '@/providers/query-provider';
import type { Route } from './+types/mailto-handler';
import { authProxy } from '@/lib/auth-proxy';
// Function to parse mailto URLs
async function parseMailtoUrl(mailtoUrl: string) {
@@ -194,7 +194,7 @@ async function createDraftFromMailto(mailtoData: {
try {
console.log(`Attempt ${attempt} to create draft...`);
const result = await serverTrpc().drafts.create(draftData);
const result = await trpcClient.drafts.create.mutate(draftData);
if (result?.id) {
console.log('Draft created successfully with ID:', result.id);
@@ -246,35 +246,39 @@ async function createDraftFromMailto(mailtoData: {
return null;
}
export async function mailtoHandler() {
const c = getContext<HonoContext>();
if (!c.var.session?.user) return c.redirect(`${c.env.NEXT_PUBLIC_APP_URL}/login`);
export async function loader({ request }: Route.LoaderArgs) {
const session = await authProxy.api.getSession({ headers: request.headers });
if (!session) return Response.redirect(`${import.meta.env.VITE_PUBLIC_APP_URL}/login`);
const url = new URL(request.url);
// Get the mailto parameter from the URL
const mailto = c.req.query('mailto');
const mailto = url.searchParams.get('mailto');
if (!mailto) return c.redirect(`${c.env.NEXT_PUBLIC_APP_URL}/mail/compose`);
if (!mailto) return Response.redirect(`${import.meta.env.VITE_PUBLIC_APP_URL}/mail/compose`);
// Parse the mailto URL
const mailtoData = await parseMailtoUrl(mailto);
// If parsing failed, redirect to empty compose
if (!mailtoData) return c.redirect(`${c.env.NEXT_PUBLIC_APP_URL}/mail/compose`);
if (!mailtoData) return Response.redirect(`${import.meta.env.VITE_PUBLIC_APP_URL}/mail/compose`);
// Create a draft from the mailto data
const draftId = await createDraftFromMailto(mailtoData);
// If draft creation failed, redirect to empty compose with the parsed data as a fallback
if (!draftId) {
const fallbackUrl = new URL(`${c.env.NEXT_PUBLIC_APP_URL}/mail/compose`);
const fallbackUrl = new URL(`${import.meta.env.VITE_PUBLIC_APP_URL}/mail/compose`);
if (mailtoData.to) fallbackUrl.searchParams.append('to', mailtoData.to);
if (mailtoData.subject) fallbackUrl.searchParams.append('subject', mailtoData.subject);
if (mailtoData.body) fallbackUrl.searchParams.append('body', mailtoData.body);
if (mailtoData.cc) fallbackUrl.searchParams.append('cc', mailtoData.cc);
if (mailtoData.bcc) fallbackUrl.searchParams.append('bcc', mailtoData.bcc);
return c.redirect(fallbackUrl.toString());
return Response.redirect(fallbackUrl.toString());
}
// Redirect to compose with the draft ID
return c.redirect(`${c.env.NEXT_PUBLIC_APP_URL}/mail/compose?draftId=${draftId}`);
return Response.redirect(
`${import.meta.env.VITE_PUBLIC_APP_URL}/mail/compose?draftId=${draftId}`,
);
}

View File

@@ -1,7 +1,5 @@
import type { MetadataRoute } from 'next';
export default function manifest(): MetadataRoute.Manifest {
return {
export function loader() {
return Response.json({
name: 'Zero',
short_name: '0',
description: 'Zero - the first open source email app that puts your privacy and safety first.',
@@ -15,14 +13,12 @@ export default function manifest(): MetadataRoute.Manifest {
src: '/icons-pwa/icon-512.png',
sizes: '512x512',
type: 'image/png',
// @ts-expect-error, not sure why error?
purpose: 'any maskable',
},
{
src: '/icons-pwa/icon-192.png',
sizes: '192x192',
type: 'image/png',
// @ts-expect-error, not sure why error?
purpose: 'any maskable',
},
{
@@ -31,5 +27,5 @@ export default function manifest(): MetadataRoute.Manifest {
type: 'image/png',
},
],
};
});
}

View File

@@ -0,0 +1,9 @@
export function loader() {
return Response.json({
associatedApplications: [
{
applicationId: 'ecf043a0-41bb-4c89-bd31-7e3f272f8e3c',
},
],
});
}

View File

@@ -0,0 +1,9 @@
export function loader() {
return Response.json({
associatedApplications: [
{
applicationId: '80b11343-e52c-4969-81e8-faebfed78a67',
},
],
});
}

View File

@@ -0,0 +1,7 @@
export function loader() {
throw new Response('Not Found', { status: 404 });
}
export default function NotFound() {
return null;
}

View File

@@ -1,3 +0,0 @@
import { NotFound } from './(error)/not-found';
export default NotFound;

View File

@@ -1,16 +1,15 @@
import { ImageResponse } from 'next/og';
import type { Route } from './+types/create';
import { ImageResponse } from 'workers-og';
export const runtime = 'edge';
export async function GET(request: Request) {
export async function loader({ request }: Route.LoaderArgs) {
// Get URL parameters
const { searchParams } = new URL(request.url);
const toParam = searchParams.get('to') || 'someone';
const subjectParam = searchParams.get('subject') || '';
// Use the email directly
const recipient = toParam;
// Load fonts
async function loadGoogleFont(font: string, weight: string) {
const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}&display=swap`;
@@ -32,7 +31,7 @@ export async function GET(request: Request) {
const logoSvg = `<svg width="191" height="191" viewBox="0 0 191 191" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M38.125 190.625V152.5H0V38.125H38.125V0H152.5V38.125H190.625V152.5H152.5V190.625H38.125ZM38.125 114.375H76.25V150.975H152.5V76.25H114.375V114.375H76.25V76.25H114.375V39.65H38.125V114.375Z" fill="white"/>
</svg>`;
const logoDataUrl = `data:image/svg+xml;base64,${Buffer.from(logoSvg).toString('base64')}`;
const fontWeight400 = await loadGoogleFont('Geist', '400');
@@ -54,11 +53,10 @@ export async function GET(request: Request) {
<span tw="text-[#A1A1A1] ml-3">{recipient}</span>
<span tw="text-[#fff]">on Zero</span>
</div>
</div>
<div tw="text-[36px] text-center text-neutral-400 mt-10" style={{ fontFamily: 'light' }}>
{subjectParam
{subjectParam
? `Subject: ${subjectParam.length > 50 ? subjectParam.substring(0, 47) + '...' : subjectParam}`
: 'Compose a new email'}
</div>
@@ -113,4 +111,4 @@ export async function GET(request: Request) {
],
},
);
}
}

View File

@@ -1,9 +1,6 @@
import { ImageResponse } from 'next/og';
import { env } from '@/lib/env';
import { ImageResponse } from 'workers-og';
export const runtime = 'edge';
export async function GET() {
export async function loader() {
async function loadGoogleFont(font: string, weight: string) {
const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}&display=swap`;
const css = await (await fetch(url)).text();
@@ -20,18 +17,10 @@ export async function GET() {
}
try {
const appUrl = env.NEXT_PUBLIC_APP_URL;
if (!appUrl) {
throw new Error('NEXT_PUBLIC_APP_URL is not defined');
}
const mailResponse = await fetch(new URL(`${appUrl}/white-icon.svg`));
if (!mailResponse.ok) {
throw new Error('Failed to fetch SVG');
}
const mailBuffer = await mailResponse.arrayBuffer();
const mailBase64 = btoa(String.fromCharCode(...new Uint8Array(mailBuffer)));
const mailBuffer = await fetch(
new URL(`${import.meta.env.VITE_PUBLIC_APP_URL}/white-icon.svg`),
).then((res) => res.arrayBuffer());
const mailBase64 = Buffer.from(mailBuffer).toString('base64');
const mail = `data:image/svg+xml;base64,${mailBase64}`;
const fontWeight400 = await loadGoogleFont('Geist', '400');

View File

@@ -1,15 +1,14 @@
import HomeContent from '@/components/home/HomeContent';
import { authProxy } from '@/lib/auth-proxy';
import { redirect } from 'next/navigation';
import { headers } from 'next/headers';
import type { Route } from './+types/page';
import { redirect } from 'react-router';
export default async function Home() {
const headersList = new Headers(Object.fromEntries(await (await headers()).entries()));
const session = await authProxy.api.getSession({ headers: headersList });
if (session?.connectionId) {
redirect('/mail/inbox');
}
export async function loader({ request }: Route.LoaderArgs) {
const session = await authProxy.api.getSession({ headers: request.headers });
if (session?.connectionId) throw redirect('/mail/inbox');
return null;
}
export default function Home() {
return <HomeContent />;
}

163
apps/mail/app/root.tsx Normal file
View File

@@ -0,0 +1,163 @@
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
useNavigate,
type MetaFunction,
} from 'react-router';
import { ClientProviders } from '@/providers/client-providers';
import { ServerProviders } from '@/providers/server-providers';
import { useEffect, type PropsWithChildren } from 'react';
import { Button } from '@/components/ui/button';
import { siteConfig } from '@/lib/site-config';
import { resolveLocale } from '@/i18n/request';
import { getMessages } from '@/i18n/request';
import { signOut } from '@/lib/auth-client';
import type { Route } from './+types/root';
import { AlertCircle } from 'lucide-react';
import { useTranslations } from 'use-intl';
import { ArrowLeft } from 'lucide-react';
import './globals.css';
export const meta: MetaFunction = () => {
return [
{ title: siteConfig.title },
{ name: 'description', content: siteConfig.description },
{ property: 'og:title', content: siteConfig.title },
{ property: 'og:description', content: siteConfig.description },
{ property: 'og:image', content: siteConfig.openGraph.images[0].url },
{ property: 'og:url', content: siteConfig.alternates.canonical },
{ rel: 'manifest', href: '/manifest.webmanifest' },
];
};
export async function loader({ request }: Route.LoaderArgs) {
const locale = resolveLocale(request);
return {
locale,
messages: await getMessages(locale),
};
}
export function Layout({ children }: PropsWithChildren) {
const { locale, messages } = useLoaderData<typeof loader>();
return (
<html lang={locale} suppressHydrationWarning>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body className="antialiased">
<ServerProviders messages={messages} locale={locale}>
<ClientProviders>{children}</ClientProviders>
</ServerProviders>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = 'Oops!';
let details = 'An unexpected error occurred.';
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? '404' : 'Error';
details =
error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
if (error.status === 404) {
return <NotFound />;
}
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}
useEffect(() => {
console.error(error);
console.error({ message, details, stack });
}, [error, message, details, stack]);
return (
<div className="dark:bg-background flex w-full items-center justify-center bg-white text-center">
<div className="flex-col items-center justify-center md:flex dark:text-gray-100">
{/* Message */}
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">Something went wrong!</h2>
<p className="text-muted-foreground">See the console for more information.</p>
<pre className="text-muted-foreground">{JSON.stringify(error, null, 2)}</pre>
</div>
<div className="mt-2 flex gap-2">
<Button
variant="outline"
onClick={() => window.location.reload()}
className="text-muted-foreground gap-2"
>
Refresh
</Button>
<Button
variant="outline"
onClick={async () => {
await signOut();
window.location.href = '/login';
}}
className="text-muted-foreground gap-2"
>
Log Out and Refresh
</Button>
</div>
</div>
</div>
);
}
function NotFound() {
const navigate = useNavigate();
const t = useTranslations();
return (
<div className="dark:bg-background flex w-full items-center justify-center bg-white text-center">
<div className="flex-col items-center justify-center md:flex dark:text-gray-100">
<div className="relative">
<h1 className="text-muted-foreground/20 select-none text-[150px] font-bold">404</h1>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<AlertCircle className="text-muted-foreground h-20 w-20" />
</div>
</div>
{/* Message */}
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">
{t('pages.error.notFound.title')}
</h2>
<p className="text-muted-foreground">{t('pages.error.notFound.description')}</p>
</div>
{/* Buttons */}
<div className="mt-2 flex gap-2">
<Button
variant="outline"
onClick={() => navigate(-1)}
className="text-muted-foreground gap-2"
>
<ArrowLeft className="h-4 w-4" />
{t('pages.error.notFound.goBack')}
</Button>
</div>
</div>
</div>
);
}

66
apps/mail/app/routes.ts Normal file
View File

@@ -0,0 +1,66 @@
import { type RouteConfig, index, layout, prefix, route } from '@react-router/dev/routes';
export default [
index('page.tsx'),
route('/home', 'home/page.tsx'),
route('/manifest.webmanifest', 'meta-files/manifest.ts'),
route(
'/.well-known/microsoft-identity-association',
'meta-files/microsoft-identity-association.ts',
),
route(
'/.well-known/microsoft-identity-association.json',
'meta-files/microsoft-identity-association.json.ts',
),
route('/api/mailto-handler', 'mailto-handler.ts'),
route('/og-api/home', 'og-api/home.tsx'),
route('/og-api/create', 'og-api/create.tsx'),
layout('(full-width)/layout.tsx', [
route('/about', '(full-width)/about.tsx'),
route('/terms', '(full-width)/terms.tsx'),
route('/pricing', '(full-width)/pricing.tsx'),
route('/privacy', '(full-width)/privacy.tsx'),
route('/contributors', '(full-width)/contributors.tsx'),
]),
route('/login', '(auth)/login/page.tsx'),
// Enable this when we have a zero signup page
// route('/zero/signup', '(auth)/zero/signup/page.tsx'),
// route('/zero/login', '(auth)/zero/login/page.tsx'),
layout('(routes)/layout.tsx', [
route('/developer', '(routes)/developer/page.tsx'),
layout(
'(routes)/mail/layout.tsx',
prefix('/mail', [
index('(routes)/mail/page.tsx'),
route('/create', '(routes)/mail/create/page.tsx'),
route('/compose', '(routes)/mail/compose/page.tsx'),
route('/under-construction/:path', '(routes)/mail/under-construction/[path]/page.tsx'),
route('/:folder', '(routes)/mail/[folder]/page.tsx'),
]),
),
layout(
'(routes)/settings/layout.tsx',
prefix('/settings', [
index('(routes)/settings/page.tsx'),
route('/appearance', '(routes)/settings/appearance/page.tsx'),
route('/connections', '(routes)/settings/connections/page.tsx'),
route('/danger-zone', '(routes)/settings/danger-zone/page.tsx'),
route('/general', '(routes)/settings/general/page.tsx'),
route('/labels', '(routes)/settings/labels/page.tsx'),
route('/notifications', '(routes)/settings/notifications/page.tsx'),
route('/privacy', '(routes)/settings/privacy/page.tsx'),
route('/security', '(routes)/settings/security/page.tsx'),
route('/shortcuts', '(routes)/settings/shortcuts/page.tsx'),
route('/*', '(routes)/settings/[...settings]/page.tsx'),
]),
),
]),
// 404 page
route('/*', 'meta-files/not-found.ts'),
] satisfies RouteConfig;

View File

@@ -1,7 +1,6 @@
import { Tooltip, TooltipTrigger, TooltipContent } from './ui/tooltip';
import { useAISidebar } from './ui/ai-sidebar';
import { Button } from './ui/button';
import Image from 'next/image';
// AI Toggle Button Component
const AIToggleButton = () => {
@@ -24,14 +23,14 @@ const AIToggleButton = () => {
}}
>
<div className="flex items-center justify-center">
<Image
<img
src="/black-icon.svg"
alt="AI Assistant"
width={22}
height={22}
className="block dark:hidden"
/>
<Image
<img
src="/white-icon.svg"
alt="AI Assistant"
width={22}

View File

@@ -9,9 +9,9 @@ import {
import { useBilling } from '@/hooks/use-billing';
import { emailProviders } from '@/lib/constants';
import { authClient } from '@/lib/auth-client';
import { usePathname } from 'next/navigation';
import { Plus, UserPlus } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useLocation } from 'react-router';
import { useTranslations } from 'use-intl';
import { Button } from '../ui/button';
import { motion } from 'motion/react';
import { cn } from '@/lib/utils';
@@ -34,7 +34,7 @@ export const AddConnectionDialog = ({
if (!connections?.remaining && !connections?.unlimited) return false;
return (connections?.unlimited && !connections?.remaining) || (connections?.remaining ?? 0) > 0;
}, [connections]);
const pathname = usePathname();
const pathname = useLocation().pathname;
const handleUpgrade = async () => {
if (attach) {
@@ -53,20 +53,19 @@ export const AddConnectionDialog = ({
return (
<Dialog onOpenChange={onOpenChange}>
<DialogTrigger asChild>
{children || (
<Button
size={'dropdownItem'}
variant={'dropdownItem'}
className={cn('w-full justify-start gap-2', className)}
>
<UserPlus size={16} strokeWidth={2} className="opacity-60" aria-hidden="true" />
<p className="text-[13px] opacity-60">{t('pages.settings.connections.addEmail')}</p>
</Button>
)}
</DialogTrigger>
<DialogContent showOverlay={true}>
<DialogTrigger asChild>
{children || (
<Button
size={'dropdownItem'}
variant={'dropdownItem'}
className={cn('w-full justify-start gap-2', className)}
>
<UserPlus size={16} strokeWidth={2} className="opacity-60" aria-hidden="true" />
<p className="text-[13px] opacity-60">{t('pages.settings.connections.addEmail')}</p>
</Button>
)}
</DialogTrigger>
<DialogContent showOverlay={true}>
<DialogHeader>
<DialogTitle>{t('pages.settings.connections.connectEmail')}</DialogTitle>
<DialogDescription>

View File

@@ -1,5 +1,3 @@
'use client';
import {
CommandDialog,
CommandEmpty,
@@ -11,11 +9,14 @@ import {
CommandShortcut,
} from '@/components/ui/command';
import { DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import { useRouter, usePathname } from 'next/navigation';
import { navigationConfig } from '@/config/navigation';
import { useOpenComposeModal } from '@/hooks/use-open-compose-modal';
import { navigationConfig, type NavItem } from '@/config/navigation';
import { useNavigate, useLocation } from 'react-router';
import { keyboardShortcuts } from '@/config/shortcuts';
import { ArrowUpRight } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import { CircleHelp } from 'lucide-react';
import { VisuallyHidden } from 'radix-ui';
import { Pencil2 } from '../icons/icons';
import { useQueryState } from 'nuqs';
import * as React from 'react';
@@ -53,8 +54,8 @@ export function useCommandPalette() {
export function CommandPalette({ children }: { children: React.ReactNode }) {
const [open, setOpen] = React.useState(false);
const [, setIsComposeOpen] = useQueryState('isComposeOpen');
const router = useRouter();
const pathname = usePathname();
const navigate = useNavigate();
const { pathname } = useLocation();
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
@@ -167,10 +168,10 @@ export function CommandPalette({ children }: { children: React.ReactNode }) {
}}
>
<CommandDialog open={open} onOpenChange={setOpen}>
<VisuallyHidden>
<VisuallyHidden.VisuallyHidden>
<DialogTitle>{t('common.commandPalette.title')}</DialogTitle>
<DialogDescription>{t('common.commandPalette.description')}</DialogDescription>
</VisuallyHidden>
</VisuallyHidden.VisuallyHidden>
<CommandInput autoFocus placeholder={t('common.commandPalette.placeholder')} />
<CommandList>
<CommandEmpty>{t('common.commandPalette.noResults')}</CommandEmpty>
@@ -186,7 +187,7 @@ export function CommandPalette({ children }: { children: React.ReactNode }) {
if (item.onClick) {
item.onClick();
} else if (item.url) {
router.push(item.url);
navigate(item.url);
}
})
}

View File

@@ -17,7 +17,7 @@ import { useTRPC } from '@/providers/query-provider';
import { useMutation } from '@tanstack/react-query';
import { useState, type ReactNode } from 'react';
import { useLabels } from '@/hooks/use-labels';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import { Trash } from '../icons/icons';
import { Button } from '../ui/button';
import { toast } from 'sonner';

View File

@@ -1,5 +1,3 @@
'use client';
import {
SIDEBAR_COOKIE_MAX_AGE,
SIDEBAR_COOKIE_NAME,

View File

@@ -30,14 +30,14 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { backgroundQueueAtom } from '@/store/backgroundQueue';
import { useThread, useThreads } from '@/hooks/use-threads';
import { useSearchValue } from '@/hooks/use-search-value';
import { useParams, useRouter } from 'next/navigation';
import { useParams, useNavigate } from 'react-router';
import { useTRPC } from '@/providers/query-provider';
import { ExclamationCircle } from '../icons/icons';
import { useLabels } from '@/hooks/use-labels';
import { LABELS, FOLDERS } from '@/lib/utils';
import { useStats } from '@/hooks/use-stats';
import { useTranslations } from 'next-intl';
import { useMail } from '../mail/use-mail';
import { useTranslations } from 'use-intl';
import { Checkbox } from '../ui/checkbox';
import { type ReactNode } from 'react';
import { useQueryState } from 'nuqs';
@@ -122,6 +122,7 @@ export function ThreadContextMenu({
refreshCallback,
}: EmailContextMenuProps) {
const { folder } = useParams<{ folder: string }>();
const navigate = useNavigate();
const [mail, setMail] = useMail();
const [{ refetch, isLoading, isFetching }, threads] = useThreads();
const currentFolder = folder ?? '';

View File

@@ -1,5 +1,3 @@
'use client';
import {
Dialog,
DialogContent,

View File

@@ -1,8 +1,5 @@
'use client';
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar';
import { useRef, useCallback, useEffect } from 'react';
import { useTRPC } from '@/providers/query-provider';
import { PricingDialog } from '../ui/pricing-dialog';
import { Markdown } from '@react-email/components';
import { useAIFullScreen } from '../ui/ai-sidebar';
@@ -10,7 +7,6 @@ import { CurvedArrow, Stop } from '../icons/icons';
import { useBilling } from '@/hooks/use-billing';
import { TextShimmer } from '../ui/text-shimmer';
import { useThread } from '@/hooks/use-threads';
import { useLabels } from '@/hooks/use-labels';
import { MailLabels } from '../mail/mail-list';
import { cn, getEmailLogo } from '@/lib/utils';
import { Button } from '../ui/button';
@@ -19,8 +15,6 @@ import { useQueryState } from 'nuqs';
import { Input } from '../ui/input';
import { useState } from 'react';
import VoiceChat from './voice';
import Image from 'next/image';
import { toast } from 'sonner';
const renderThread = (thread: { id: string; title: string; snippet: string }) => {
const [, setThreadId] = useQueryState('threadId');
@@ -205,8 +199,8 @@ export function AIChat({
) : !messages.length ? (
<div className="absolute inset-0 flex flex-col items-center justify-center">
<div className="relative mb-4 h-[44px] w-[44px]">
<Image src="/black-icon.svg" alt="Zero Logo" fill className="dark:hidden" />
<Image src="/white-icon.svg" alt="Zero Logo" fill className="hidden dark:block" />
<img src="/black-icon.svg" alt="Zero Logo" className="dark:hidden" />
<img src="/white-icon.svg" alt="Zero Logo" className="hidden dark:block" />
</div>
<p className="mb-1 mt-2 hidden text-center text-sm font-medium text-black md:block dark:text-white">
Ask anything about your emails

View File

@@ -1,5 +1,3 @@
'use client';
import { cn } from '@/lib/utils';
import React from 'react';

View File

@@ -1,4 +1,3 @@
'use client';
import { Dialog, DialogClose } from '@/components/ui/dialog';
import { useEmailAliases } from '@/hooks/use-email-aliases';
import { useConnections } from '@/hooks/use-connections';
@@ -11,8 +10,8 @@ import { EmailComposer } from './email-composer';
import { useSession } from '@/lib/auth-client';
import { serializeFiles } from '@/lib/schemas';
import { useDraft } from '@/hooks/use-drafts';
import { useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { useNavigate } from 'react-router';
import { useTranslations } from 'use-intl';
import { useQueryState } from 'nuqs';
import { X } from '../icons/icons';
import posthog from 'posthog-js';
@@ -63,7 +62,7 @@ export function CreateEmail({
error: draftError,
} = useDraft(draftId ?? propDraftId ?? null);
const t = useTranslations();
const router = useRouter();
const navigate = useNavigate();
const { enableScope, disableScope } = useHotkeysContext();
const [isDraftFailed, setIsDraftFailed] = useState(false);
const trpc = useTRPC();

View File

@@ -1,6 +1,4 @@
'use client';
import { Popover, PopoverTrigger } from '@radix-ui/react-popover';
import { PopoverContent } from '@/components/ui/popover';
import { PopoverContent, Popover, PopoverTrigger } from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { Check, Trash } from 'lucide-react';
import { useEffect, useRef } from 'react';

View File

@@ -11,9 +11,8 @@ import {
CheckSquare,
type LucideIcon,
} from 'lucide-react';
import { PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { PopoverContent, PopoverTrigger, Popover } from '@/components/ui/popover';
import { EditorBubbleItem, useEditor } from 'novel';
import { Popover } from '@radix-ui/react-popover';
import { Button } from '@/components/ui/button';
import { type Editor } from '@tiptap/react';

View File

@@ -1,5 +1,3 @@
'use client';
import {
Bold,
Italic,
@@ -52,8 +50,8 @@ import { TextSelection } from 'prosemirror-state';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { EditorView } from 'prosemirror-view';
import { useTranslations } from 'next-intl';
import { Markdown } from 'tiptap-markdown';
import { useTranslations } from 'use-intl';
import { Slice } from 'prosemirror-model';
import { useState } from 'react';
import React from 'react';

View File

@@ -12,8 +12,8 @@ import { TextEffect } from '@/components/motion-primitives/text-effect';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
import useComposeEditor from '@/hooks/use-compose-editor';
import { Loader, Check, X as XIcon } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Command, Paperclip, Plus } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Avatar, AvatarFallback } from '../ui/avatar';
import { useTRPC } from '@/providers/query-provider';

View File

@@ -15,22 +15,21 @@ import {
TiptapImage,
TiptapLink,
TiptapUnderline,
Twitter,
UpdatedImage,
Youtube,
Mathematics
} from 'novel'
import { UploadImagesPlugin } from 'novel'
Mathematics,
} from 'novel';
import { UploadImagesPlugin } from 'novel';
import { cx } from 'class-variance-authority'
import { common, createLowlight } from 'lowlight'
import { common, createLowlight } from 'lowlight';
import { cx } from 'class-variance-authority';
//TODO I am using cx here to get tailwind autocomplete working, idk if someone else can write a regex to just capture the class key in objects
const aiHighlight = AIHighlight
const aiHighlight = AIHighlight;
//You can overwrite the placeholder with your own configuration
const placeholder = Placeholder
const placeholder = Placeholder;
// Custom link extension that exits the link mark when space is typed
import { Extension } from '@tiptap/core'
import { Extension } from '@tiptap/core';
// Create a separate extension to handle exiting links on space
const ExitLinkOnSpace = Extension.create({
@@ -41,143 +40,141 @@ const ExitLinkOnSpace = Extension.create({
if (editor.isActive('link')) {
// Insert a space character first
editor.commands.insertContent(' ');
// Then explicitly unset the link mark
editor.commands.unsetLink();
return true;
}
return false;
}
},
};
},
})
});
// Configure the link extension with standard options
const tiptapLink = TiptapLink.configure({
HTMLAttributes: {
class: cx(
'text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer'
)
'text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer',
),
},
openOnClick: false,
autolink: true,
linkOnPaste: true,
protocols: ['http', 'https', 'mailto', 'tel']
})
protocols: ['http', 'https', 'mailto', 'tel'],
});
const tiptapImage = TiptapImage.extend({
addProseMirrorPlugins() {
return [
UploadImagesPlugin({
imageClass: cx('opacity-40 rounded-lg border border-stone-200')
})
]
}
imageClass: cx('opacity-40 rounded-lg border border-stone-200'),
}),
];
},
}).configure({
allowBase64: true,
HTMLAttributes: {
class: cx('rounded-lg border border-muted')
}
})
class: cx('rounded-lg border border-muted'),
},
});
const updatedImage = UpdatedImage.configure({
HTMLAttributes: {
class: cx('rounded-lg border border-muted')
}
})
class: cx('rounded-lg border border-muted'),
},
});
const taskList = TaskList.configure({
HTMLAttributes: {
class: cx('not-prose pl-2 ')
}
})
class: cx('not-prose pl-2 '),
},
});
const taskItem = TaskItem.configure({
HTMLAttributes: {
class: cx('flex gap-2 items-start my-4')
class: cx('flex gap-2 items-start my-4'),
},
nested: true
})
nested: true,
});
const horizontalRule = HorizontalRule.configure({
HTMLAttributes: {
class: cx('mt-4 mb-6 border-t border-muted-foreground')
}
})
class: cx('mt-4 mb-6 border-t border-muted-foreground'),
},
});
const starterKit = StarterKit.configure({
bulletList: {
HTMLAttributes: {
class: cx('list-disc list-outside leading-3 -mt-2')
}
class: cx('list-disc list-outside leading-3 -mt-2'),
},
},
orderedList: {
HTMLAttributes: {
class: cx('list-decimal list-outside leading-3 -mt-2')
}
class: cx('list-decimal list-outside leading-3 -mt-2'),
},
},
listItem: {
HTMLAttributes: {
class: cx('leading-normal -mb-2')
}
class: cx('leading-normal -mb-2'),
},
},
blockquote: {
HTMLAttributes: {
class: cx('border-l-4 border-primary')
}
class: cx('border-l-4 border-primary'),
},
},
codeBlock: {
HTMLAttributes: {
class: cx(
'rounded-md bg-muted text-muted-foreground border p-5 font-mono font-medium'
)
}
class: cx('rounded-md bg-muted text-muted-foreground border p-5 font-mono font-medium'),
},
},
code: {
HTMLAttributes: {
class: cx('rounded-md bg-muted px-1.5 py-1 font-mono font-medium'),
spellcheck: 'false'
}
spellcheck: 'false',
},
},
horizontalRule: false,
dropcursor: {
color: '#DBEAFE',
width: 4
width: 4,
},
gapcursor: false
})
gapcursor: false,
});
const codeBlockLowlight = CodeBlockLowlight.configure({
// configure lowlight: common / all / use highlightJS in case there is a need to specify certain language grammars only
// common: covers 37 language grammars which should be good enough in most cases
lowlight: createLowlight(common)
})
lowlight: createLowlight(common),
});
const youtube = Youtube.configure({
HTMLAttributes: {
class: cx('rounded-lg border border-muted')
class: cx('rounded-lg border border-muted'),
},
inline: false
})
inline: false,
});
const twitter = Twitter.configure({
HTMLAttributes: {
class: cx('not-prose')
},
inline: false
})
// const twitter = Twitter.configure({
// HTMLAttributes: {
// class: cx('not-prose')
// },
// inline: false
// })
const mathematics = Mathematics.configure({
HTMLAttributes: {
class: cx('text-foreground rounded p-1 hover:bg-accent cursor-pointer')
class: cx('text-foreground rounded p-1 hover:bg-accent cursor-pointer'),
},
katexOptions: {
throwOnError: false
}
})
throwOnError: false,
},
});
const characterCount = CharacterCount.configure()
const characterCount = CharacterCount.configure();
export const defaultExtensions = [
starterKit,
@@ -192,7 +189,6 @@ export const defaultExtensions = [
aiHighlight,
codeBlockLowlight,
youtube,
twitter,
mathematics,
characterCount,
TiptapUnderline,
@@ -200,5 +196,5 @@ export const defaultExtensions = [
TextStyle,
Color,
CustomKeymap,
GlobalDragHandle
]
GlobalDragHandle,
];

View File

@@ -1,12 +1,10 @@
import { PopoverContent, Popover, PopoverTrigger } from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { Check, Trash } from 'lucide-react';
import { useEffect, useRef } from 'react';
import { useEditor } from 'novel';
import { cn } from '@/lib/utils';
import { Popover, PopoverTrigger } from '@radix-ui/react-popover';
import { PopoverContent } from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
export function isValidUrl(url: string) {
try {
new URL(url);

View File

@@ -11,10 +11,8 @@ import {
TextIcon,
TextQuote,
} from 'lucide-react';
import { PopoverContent, PopoverTrigger, Popover } from '@/components/ui/popover';
import { EditorBubbleItem, useEditor } from 'novel';
import { PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Popover } from '@radix-ui/react-popover';
import { Button } from '@/components/ui/button';
export type SelectorItem = {

View File

@@ -0,0 +1,61 @@
import { MessageSquare, FileText, Edit } from 'lucide-react';
import type { SelectorItem } from './node-selector';
import { EditorBubbleItem, useEditor } from 'novel';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
export const TextButtons = () => {
const { editor } = useEditor();
if (!editor) return null;
// Define AI action handlers
const handleChatWithAI = () => {
// Get selected text
const selection = editor.state.selection;
const selectedText = selection.empty
? ''
: editor.state.doc.textBetween(selection.from, selection.to);
console.log('Chat with AI about:', selectedText);
// Implement chat with AI functionality
};
const items = [
{
name: 'chat-with-zero',
label: 'Chat with Zero',
action: handleChatWithAI,
useImage: true,
imageSrc: '/ai.svg',
},
];
return (
<div className="flex">
{items.map((item) => (
<EditorBubbleItem
key={item.name}
onSelect={() => {
item.action();
}}
>
<Button size="sm" className="flex items-center gap-1.5 rounded-none px-3" variant="ghost">
{
item.useImage ? (
<img
src={item.imageSrc}
alt={item.label}
width={16}
height={16}
className="h-4 w-4"
/>
) : null
// <item.icon className="h-4 w-4" />
}
<span className="text-xs">{item.label}</span>
</Button>
</EditorBubbleItem>
))}
</div>
);
};

View File

@@ -1,8 +1,6 @@
'use client';
import { useImageLoading } from '@/hooks/use-image-loading';
import React, { useEffect, useRef } from 'react';
import DOMPurify from 'dompurify';
import { useImageLoading } from '@/hooks/use-image-loading';
interface SignatureDisplayProps {
html: string;
@@ -14,18 +12,18 @@ export default function SignatureDisplay({ html, className = '' }: SignatureDisp
useEffect(() => {
if (!iframeRef.current) return;
const iframe = iframeRef.current;
try {
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (!doc) return;
const sanitizedHtml = DOMPurify.sanitize(html, {
ADD_ATTR: ['target'],
FORBID_TAGS: ['script', 'iframe', 'object', 'embed'],
});
doc.open();
doc.write(`
<!DOCTYPE html>
@@ -49,7 +47,7 @@ export default function SignatureDisplay({ html, className = '' }: SignatureDisp
</html>
`);
doc.close();
// Use a more efficient approach for height measurement
const measureAndSetHeight = () => {
if (doc.body) {
@@ -59,16 +57,16 @@ export default function SignatureDisplay({ html, className = '' }: SignatureDisp
}
}
};
// Set height after content loads
measureAndSetHeight();
// Set up listeners for images that might change the height
const cleanup = useImageLoading(doc, measureAndSetHeight);
// Final height check after a short delay
const timeoutId = setTimeout(measureAndSetHeight, 100);
return () => {
cleanup();
clearTimeout(timeoutId);
@@ -87,4 +85,4 @@ export default function SignatureDisplay({ html, className = '' }: SignatureDisp
sandbox="allow-same-origin"
/>
);
}
}

View File

@@ -1,6 +1,5 @@
import { Button } from '@/components/ui/button';
import { FileIcon, X } from 'lucide-react';
import Image from 'next/image';
import React from 'react';
const getLogo = (mimetype: string): string => {
@@ -37,7 +36,7 @@ export const UploadedFileIcon = ({ removeAttachment, index, file }: Props) => {
<div className="relative h-24 w-full">
{file.type.startsWith('image/') ? (
<>
<Image src={URL.createObjectURL(file)} alt={file.name} fill className="object-cover" />
<img src={URL.createObjectURL(file)} alt={file.name} className="object-cover" />
<Button
variant="ghost"
size="icon"
@@ -50,7 +49,7 @@ export const UploadedFileIcon = ({ removeAttachment, index, file }: Props) => {
) : (
<div className="bg-muted/20 flex h-full w-full items-center justify-center">
{getLogo(file.type) && (
<Image src={getLogo(file.type)} alt={file.name} width={80} height={80} />
<img src={getLogo(file.type)} alt={file.name} width={80} height={80} />
)}
<Button
variant="ghost"

View File

@@ -1,5 +1,3 @@
'use client';
import React, { useEffect, useState, useMemo } from 'react';
// ElevenLabs
@@ -8,18 +6,14 @@ import { useConversation } from '@11labs/react';
// UI
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Mic, MicOff, Volume2, VolumeX, XIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
// Auth
import { trpcClient } from '@/providers/query-provider';
import { useThreads } from '@/hooks/use-threads';
import { Button } from '@/components/ui/button';
import { useSession } from '@/lib/auth-client';
import type { Sender } from '@/types';
import dedent from 'dedent';
// Utils
import { env } from '@/lib/env';
interface EmailContent {
metadata: {
isUnread: boolean;
@@ -132,7 +126,7 @@ const VoiceChat = ({ onClose }: VoiceChatProps) => {
const emailContext = emailContent.join('\n\n');
const conversationId = await conversation.startSession({
agentId: env.NEXT_PUBLIC_ELEVENLABS_AGENT_ID,
agentId: import.meta.env.VITE_PUBLIC_ELEVENLABS_AGENT_ID!,
dynamicVariables: {
user_name: userName,
email_context: emailContext,

View File

@@ -1,5 +1,3 @@
'use client';
import {
ArrowRight,
ChevronDown,
@@ -46,22 +44,19 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/co
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Form, FormControl, FormField, FormItem } from '@/components/ui/form';
import { use, useCallback, useEffect, useRef, useState } from 'react';
import { useInView, motion } from 'motion/react';
import { Button } from '@/components/ui/button';
import { Balancer } from 'react-wrap-balancer';
import { Input } from '@/components/ui/input';
import { Command, Menu } from 'lucide-react';
import { Separator } from '../ui/separator';
import Balancer from 'react-wrap-balancer';
import { signIn } from '@/lib/auth-client';
import { useForm } from 'react-hook-form';
import { useInView } from 'motion/react';
import { motion } from 'framer-motion';
import { useTheme } from 'next-themes';
import { Link } from 'react-router';
import { cn } from '@/lib/utils';
import { env } from '@/lib/env';
import Image from 'next/image';
import { toast } from 'sonner';
import Footer from './footer';
import Link from 'next/link';
import React from 'react';
import { z } from 'zod';
@@ -181,12 +176,12 @@ export default function HomeContent() {
<header className="fixed z-50 hidden w-full items-center justify-center px-4 pt-6 md:flex">
<nav className="border-input/50 bg-[#1E1E1E] flex w-full max-w-3xl items-center justify-between gap-2 rounded-xl border-t p-2 px-4">
<div className="flex items-center gap-6">
<a href="/" className="relative bottom-1 cursor-pointer">
<Image src="white-icon.svg" alt="Zero Email" width={22} height={22} />
<Link to="/" className="relative bottom-1 cursor-pointer">
<img src="white-icon.svg" alt="Zero Email" width={22} height={22} />
<span className="text-muted-foreground absolute -right-[-0.5px] text-[10px]">
beta
</span>
</a>
</Link>
<NavigationMenu>
<NavigationMenuList className="gap-1">
<NavigationMenuItem>
@@ -236,7 +231,7 @@ export default function HomeContent() {
toast.promise(
signIn.social({
provider: 'google',
callbackURL: `${env.NEXT_PUBLIC_APP_URL}/mail`,
callbackURL: `${window.location.origin}/mail`,
}),
{
error: 'Login redirect failed',
@@ -265,7 +260,7 @@ export default function HomeContent() {
<SheetContent side="left" className="w-[300px] bg-[#111111] sm:w-[400px]">
<SheetHeader className="flex flex-row items-center justify-between">
<SheetTitle>
<Image src="white-icon.svg" alt="Zero Email" width={22} height={22} />
<img src="white-icon.svg" alt="Zero Email" width={22} height={22} />
</SheetTitle>
<a href="/login">
<Button className="w-full">Sign in</Button>
@@ -291,7 +286,7 @@ export default function HomeContent() {
return (
<Link
key={resource.title}
href={resource.href}
to={resource.href}
className="flex items-center gap-2 font-medium"
>
{resource.platform && <Icon className="h-5 w-5" />}
@@ -311,7 +306,7 @@ export default function HomeContent() {
className="border-input/50 mb-6 inline-flex items-center gap-4 rounded-full border border-[#2A2A2A] bg-[#1E1E1E] px-4 py-1"
>
<span className="flex items-center gap-2 text-sm">
<Image
<img
src="/yc-small.svg"
alt="Y Combinator"
className="rounded-[2px]"
@@ -358,7 +353,7 @@ export default function HomeContent() {
toast.promise(
signIn.social({
provider: 'google',
callbackURL: `${env.NEXT_PUBLIC_APP_URL}/mail`,
callbackURL: `${window.location.origin}/mail`,
}),
{
error: 'Login redirect failed',
@@ -420,13 +415,13 @@ export default function HomeContent() {
<div className="bg-border absolute -right-px -top-4 hidden h-4 w-px md:block" /> */}
{tabs.map((tab) => (
<TabsContent key={tab.value} value={tab.value}>
<Image
<img
src="/email-preview.png"
alt="Zero Email Preview"
width={1920}
height={1080}
className="relative hidden md:block"
priority
loading="eager"
/>
</TabsContent>
))}
@@ -436,13 +431,13 @@ export default function HomeContent() {
</section>
<div className="flex items-center justify-center px-4 md:hidden">
<Image
<img
src="/email-preview.png"
alt="Zero Email Preview"
width={1920}
height={1080}
className="mt-10 h-fit w-full rounded-xl border"
priority
loading="eager"
/>
</div>
@@ -483,7 +478,7 @@ export default function HomeContent() {
<div className="text-base-gray-500/50 justify-start text-sm leading-none">To:</div>
<div className="flex flex-1 items-center justify-start gap-1">
<div className="outline-tokens-badge-default/10 flex items-center justify-start gap-1.5 rounded-full border border-[#2B2B2B] py-1 pl-1 pr-1.5">
<Image
<img
height={20}
width={20}
className="h-5 w-5 rounded-full"
@@ -499,7 +494,7 @@ export default function HomeContent() {
</div>
</div>
<div className="outline-tokens-badge-default/10 flex items-center justify-start gap-1.5 rounded-full border border-[#2B2B2B] py-1 pl-1 pr-1.5">
<Image
<img
height={20}
width={20}
className="h-5 w-5 rounded-full"
@@ -701,12 +696,11 @@ export default function HomeContent() {
</div>
<div className="flex flex-col items-start justify-start gap-1.5 self-stretch px-1.5">
<div className="inline-flex items-center justify-start gap-2.5 self-stretch rounded-md p-2.5">
<Image
<img
alt="Nizzy"
height={250}
width={250}
objectFit="cover"
className="h-6 w-6 rounded-full"
className="h-6 w-6 rounded-full object-cover"
src="/nizzy.jpg"
/>
<div className="inline-flex h-7 flex-1 flex-col items-start justify-start gap-2">
@@ -945,7 +939,7 @@ export default function HomeContent() {
</div>
<div className="border-tokens-stroke-light/5 flex-col items-start justify-start gap-6 self-stretch overflow-hidden border-b-[0.35px] p-3.5">
<div className="inline-flex items-center justify-start gap-3 self-stretch">
<Image
<img
alt="Ahmet"
height={200}
width={200}
@@ -980,7 +974,7 @@ export default function HomeContent() {
<div className="absolute left-0 top-[121px] inline-flex w-[650px] flex-col items-start justify-start gap-4 overflow-hidden rounded-3xl border border-[#8B5CF6] bg-[#2A1D48] p-6 outline outline-[#3F325F]">
<div className="inline-flex items-center justify-start gap-1.5">
<div className="relative h-3.5 w-3.5">
<Image src="/star.svg" alt="AI Summary" width={16} height={16} className="" />
<img src="/star.svg" alt="AI Summary" width={16} height={16} className="" />
</div>
<div className="flex items-center justify-start gap-1 text-xs leading-3 text-[#948CA4]">
AI Summary
@@ -1040,7 +1034,7 @@ export default function HomeContent() {
<div className="inline-flex items-center justify-start gap-3 self-stretch rounded-lg p-3">
<div className="relative h-8 w-8 rounded-full bg-indigo-500/10">
<div className="absolute left-[10.2px] top-[4px] h-7 w-3 overflow-hidden">
<Image
<img
src="/stripe.svg"
alt="Stripe"
width={12}
@@ -1077,7 +1071,7 @@ export default function HomeContent() {
<div className="relative h-8 w-8 rounded-full bg-red-600/10">
<div className="absolute left-0 top-0 h-8 w-8 rounded-full" />
<div className="absolute left-[11px] top-[4px] h-7 w-2.5">
<Image
<img
src="/netflix.svg"
alt="Stripe"
width={12}
@@ -1111,7 +1105,7 @@ export default function HomeContent() {
</div>
</div>
<div className="inline-flex items-center justify-start gap-3 self-stretch rounded-[10px] bg-[#202020] p-3">
<Image
<img
className="h-8 w-8 rounded-full"
src="/dudu.jpg"
alt="Dudu"
@@ -1330,7 +1324,7 @@ export default function HomeContent() {
</div>
<div className="flex flex-col items-start justify-start gap-2 self-stretch px-2 pb-2">
<div className="inline-flex items-center justify-start gap-3 self-stretch rounded-lg p-3">
<Image
<img
src="/adam.jpg"
alt="avatar"
width={32}
@@ -1449,7 +1443,7 @@ export default function HomeContent() {
</div>
</div>
<div className="relative flex flex-1 flex-col items-center justify-center gap-8 self-stretch overflow-hidden px-5 py-4">
<Image
<img
src="/white-icon.svg"
alt="chat"
width={28}
@@ -1520,7 +1514,7 @@ export default function HomeContent() {
</div>
</div>
</motion.div>
<Image
<img
src="/pixel.svg"
alt="hero"
width={1920}
@@ -1551,21 +1545,21 @@ export default function HomeContent() {
<span className="text-white underline">save hours every week.</span>
</div>
<div className="flex items-center justify-center">
<Image
<img
className="relative bottom-12 right-[162px]"
src="/verified-home.png"
alt="tasks"
width={50}
height={50}
/>
<Image
<img
className="relative bottom-[150px] right-[-25px]"
src="/snooze-home.png"
alt="tasks"
width={50}
height={50}
/>
<Image
<img
className="relative bottom-[195px] left-[278px]"
src="/star-home.png"
alt="tasks"

View File

@@ -1,11 +1,8 @@
'use client';
import { LinkedIn, Twitter, Discord } from '../icons/icons';
import { motion, useInView } from 'motion/react';
import { Button } from '../ui/button';
import Image from 'next/image';
import { Link } from 'react-router';
import { useRef } from 'react';
import Link from 'next/link';
const socialLinks = [
{
@@ -83,12 +80,12 @@ export default function Footer() {
<div className="inline-flex flex-col items-start justify-between self-stretch">
<div className="inline-flex w-8 items-center justify-start gap-3">
<a href="/">
<Image src="/white-icon.svg" alt="logo" width={100} height={100} />
<img src="/white-icon.svg" alt="logo" width={100} height={100} />
</a>
</div>
<div className="inline-flex items-center justify-start gap-4">
{socialLinks.map((social) => (
<Link
<a
key={social.name}
href={social.href}
target="_blank"
@@ -98,7 +95,7 @@ export default function Footer() {
<div className="relative h-3.5 w-3.5 overflow-hidden">
<social.icon className="absolute h-3.5 w-3.5 fill-white" />
</div>
</Link>
</a>
))}
</div>
<div className="flex items-center justify-start gap-3">
@@ -107,7 +104,7 @@ export default function Footer() {
</div>
<a href="https://www.ycombinator.com" target="_blank" rel="noopener noreferrer">
<div className="relative w-36 overflow-hidden">
<Image
<img
src="/yc.svg"
className="bg-transparent"
alt="logo"
@@ -124,7 +121,7 @@ export default function Footer() {
Product
</div>
<div className="flex flex-col items-start justify-start gap-4 self-stretch">
<Link
<a
href="https://x.com/nizzyabi/status/1918064165530550286"
className="w-full"
target="_blank"
@@ -132,8 +129,8 @@ export default function Footer() {
<div className="justify-start self-stretch text-base leading-none text-white opacity-80 transition-opacity hover:opacity-100">
Product
</div>
</Link>
<Link
</a>
<a
href="https://x.com/nizzyabi/status/1918051282881069229"
className="w-full"
target="_blank"
@@ -141,8 +138,8 @@ export default function Footer() {
<div className="justify-start self-stretch text-base leading-none text-white opacity-80 transition-opacity hover:opacity-100">
Zero AI
</div>
</Link>
<Link
</a>
<a
href="https://x.com/nizzyabi/status/1919292505260249486"
className="w-full"
target="_blank"
@@ -150,7 +147,7 @@ export default function Footer() {
<div className="justify-start self-stretch text-base leading-none text-white opacity-80 transition-opacity hover:opacity-100">
Shortcuts
</div>
</Link>
</a>
</div>
</div>
<div className="inline-flex flex-col items-start justify-start gap-5">
@@ -185,7 +182,7 @@ export default function Footer() {
</div>
<div className="flex items-center justify-start gap-4">
<Link
href="/about"
to="/about"
className="justify-start text-sm font-normal leading-tight text-white/70 opacity-80 transition-opacity hover:opacity-100"
>
About
@@ -193,14 +190,14 @@ export default function Footer() {
<div className="h-5 w-0 outline outline-1 outline-offset-[-0.50px] outline-white/20" />
<Link
href="/terms"
to="/terms"
className="justify-start text-sm font-normal leading-tight text-white/70 opacity-80 transition-opacity hover:opacity-100"
>
Terms & Conditions
</Link>
<div className="h-5 w-0 outline outline-1 outline-offset-[-0.50px] outline-white/20" />
<Link
href="/privacy"
to="/privacy"
className="justify-start text-sm font-normal leading-tight text-white/70 opacity-80 transition-opacity hover:opacity-100"
>
Privacy Policy

View File

@@ -11,7 +11,6 @@ export function PixelatedBackground(props: SVGProps<SVGSVGElement>) {
xmlnsXlink="http://www.w3.org/1999/xlink"
{...props}
>
<g mask="url(#mask0_11_932)">
<rect
width="1438.5"

View File

@@ -1,9 +1,5 @@
import { PixelatedBackground } from "./pixelated-bg";
import { PixelatedBackground } from './pixelated-bg';
export default function Speed() {
return (
<div className="">
</div>
);
}
return <div className=""></div>;
}

View File

@@ -1,5 +1,3 @@
'use client';
import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
import type { Transition, Variants } from 'motion/react';
import { motion, useAnimation } from 'motion/react';

View File

@@ -1,5 +1,3 @@
'use client';
import type React from 'react';
import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';

View File

@@ -1,5 +1,3 @@
'use client';
import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
import { motion, useAnimation } from 'motion/react';
import type { Variants } from 'motion/react';

View File

@@ -43,16 +43,15 @@ import { useSession } from '@/lib/auth-client';
import { RenderLabels } from './render-labels';
import ReplyCompose from './reply-composer';
import { Separator } from '../ui/separator';
import { useParams } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { MailIframe } from './mail-iframe';
import { useTranslations } from 'use-intl';
import { useParams } from 'react-router';
import { MailLabels } from './mail-list';
import { FileText } from 'lucide-react';
import { format, set } from 'date-fns';
import { Button } from '../ui/button';
import { useQueryState } from 'nuqs';
import { Badge } from '../ui/badge';
import Image from 'next/image';
// Add formatFileSize utility function
const formatFileSize = (size: number) => {

View File

@@ -6,7 +6,7 @@ import { getBrowserTimezone } from '@/lib/timezones';
import { template } from '@/lib/email-utils.client';
import { useMutation } from '@tanstack/react-query';
import { useSettings } from '@/hooks/use-settings';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import { useTheme } from 'next-themes';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';

View File

@@ -1,5 +1,3 @@
'use client';
import {
Archive2,
Bell,
@@ -47,7 +45,7 @@ import { useSearchValue } from '@/hooks/use-search-value';
import { ScrollArea } from '@/components/ui/scroll-area';
import { highlightText } from '@/lib/email-utils.client';
import { useHotkeysContext } from 'react-hotkeys-hook';
import { useParams, useRouter } from 'next/navigation';
import { useParams, useNavigate } from 'react-router';
import { useTRPC } from '@/providers/query-provider';
import { useThreadLabels } from '@/hooks/use-labels';
import { useKeyState } from '@/hooks/use-hot-key';
@@ -56,14 +54,13 @@ import { RenderLabels } from './render-labels';
import { Badge } from '@/components/ui/badge';
import { useDraft } from '@/hooks/use-drafts';
import { useStats } from '@/hooks/use-stats';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import { useTheme } from 'next-themes';
import { Button } from '../ui/button';
import { useQueryState } from 'nuqs';
import { Categories } from './mail';
import items from './demo.json';
import { useAtom } from 'jotai';
import Image from 'next/image';
import { toast } from 'sonner';
const HOVER_DELAY = 1000; // ms before prefetching
@@ -749,7 +746,7 @@ export const MailList = memo(({ isCompact }: MailListProps) => {
const { folder } = useParams<{ folder: string }>();
const { data: session } = useSession();
const t = useTranslations();
const router = useRouter();
const navigate = useNavigate();
const [, setThreadId] = useQueryState('threadId');
const [, setDraftId] = useQueryState('draftId');
const [category, setCategory] = useQueryState('category');
@@ -969,7 +966,7 @@ export const MailList = memo(({ isCompact }: MailListProps) => {
) : !items || items.length === 0 ? (
<div className="flex h-[calc(100dvh-9rem)] w-full items-center justify-center md:h-[calc(100dvh-4rem)]">
<div className="flex flex-col items-center justify-center gap-2 text-center">
<Image
<img
suppressHydrationWarning
src={resolvedTheme === 'dark' ? '/empty-state.svg' : '/empty-state-light.svg'}
alt="Empty Inbox"

View File

@@ -1,15 +1,13 @@
'use client';
import { moveThreadsTo, type ThreadDestination } from '@/lib/thread-actions';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useThread, useThreads } from '@/hooks/use-threads';
import { useParams, useRouter } from 'next/navigation';
import { useParams, useNavigate } from 'react-router';
import { useTRPC } from '@/providers/query-provider';
import { Archive, Mail, Inbox } from 'lucide-react';
import { useCallback, memo, useState } from 'react';
import { Button } from '@/components/ui/button';
import { useStats } from '@/hooks/use-stats';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import { cn, FOLDERS } from '@/lib/utils';
import { useQueryState } from 'nuqs';
import { toast } from 'sonner';
@@ -50,7 +48,7 @@ export const MailQuickActions = memo(
const [{ refetch, isLoading }] = useThreads();
const { refetch: mutateStats } = useStats();
const t = useTranslations();
const router = useRouter();
const navigate = useNavigate();
const [isProcessing, setIsProcessing] = useState(false);
const [threadId, setThreadId] = useQueryState('threadId');
@@ -71,7 +69,7 @@ export const MailQuickActions = memo(
if (resetNavigation) {
resetNavigation();
}
}, [threadId, latestMessage, router, currentFolder, resetNavigation]);
}, [threadId, latestMessage, navigate, currentFolder, resetNavigation]);
const handleArchive = useCallback(
async (e?: React.MouseEvent) => {

View File

@@ -1,5 +1,5 @@
import { Archive, Copy, Maximize2, Minimize2, X, Reply, MoreVertical } from 'lucide-react';
import { Separator } from '@radix-ui/react-separator';
import { Separator } from '../ui/separator';
import { Skeleton } from '../ui/skeleton';
import { Button } from '../ui/button';
import { cn } from '@/lib/utils';

View File

@@ -1,5 +1,3 @@
'use client';
import {
Dialog,
DialogContent,
@@ -38,7 +36,7 @@ import { useSearchValue } from '@/hooks/use-search-value';
import { useConnections } from '@/hooks/use-connections';
import { MailList } from '@/components/mail/mail-list';
import { useHotkeysContext } from 'react-hotkeys-hook';
import { useParams, useRouter } from 'next/navigation';
import { useParams, useNavigate } from 'react-router';
import { useMail } from '@/components/mail/use-mail';
import { SidebarToggle } from '../ui/sidebar-toggle';
import { useBrainState } from '@/hooks/use-summary';
@@ -52,7 +50,7 @@ import { Button } from '@/components/ui/button';
import { useSession } from '@/lib/auth-client';
import { ScrollArea } from '../ui/scroll-area';
import { useStats } from '@/hooks/use-stats';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import { SearchBar } from './search-bar';
import { useQueryState } from 'nuqs';
import { useAtom } from 'jotai';
@@ -318,7 +316,7 @@ export function MailLayout() {
const [mail, setMail] = useMail();
const [, clearBulkSelection] = useAtom(clearBulkSelectionAtom);
const isMobile = useIsMobile();
const router = useRouter();
const navigate = useNavigate();
const { data: session, isPending } = useSession();
const { data: connections } = useConnections();
const t = useTranslations();
@@ -341,7 +339,7 @@ export function MailLayout() {
useEffect(() => {
if (!session?.user && !isPending) {
router.push('/login');
navigate('/login');
}
}, [session?.user, isPending]);

View File

@@ -1,4 +1,4 @@
import { useTranslations } from 'next-intl';
import { useTranslations } from 'use-intl';
import React, { useState } from 'react';
import { cn } from '@/lib/utils';

View File

@@ -1,10 +1,6 @@
'use client';
import { type LucideIcon } from 'lucide-react';
import Link from 'next/link';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { buttonVariants } from '@/components/ui/button';
import { type LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
interface NavProps {
@@ -28,7 +24,7 @@ export function Nav({ links, isCollapsed }: NavProps) {
isCollapsed ? (
<Tooltip key={index} delayDuration={0}>
<TooltipTrigger asChild>
<Link
<a
href="#"
className={cn(
buttonVariants({ variant: link.variant, size: 'icon' }),
@@ -39,7 +35,7 @@ export function Nav({ links, isCollapsed }: NavProps) {
>
<link.icon className="h-4 w-4" />
<span className="sr-only">{link.title}</span>
</Link>
</a>
</TooltipTrigger>
<TooltipContent side="right" className="flex items-center gap-4">
{link.title}
@@ -47,7 +43,7 @@ export function Nav({ links, isCollapsed }: NavProps) {
</TooltipContent>
</Tooltip>
) : (
<Link
<a
key={index}
href="#"
className={cn(
@@ -69,7 +65,7 @@ export function Nav({ links, isCollapsed }: NavProps) {
{link.label}
</span>
)}
</Link>
</a>
),
)}
</nav>

View File

@@ -66,8 +66,8 @@ import {
} from '@/components/ui/dialog';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useState, useRef, useEffect, useMemo } from 'react';
import { useTranslations, useFormatter } from 'next-intl';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useTranslations, useFormatter } from 'use-intl';
import { useTRPC } from '@/providers/query-provider';
import { Textarea } from '@/components/ui/textarea';
import { useMutation } from '@tanstack/react-query';

View File

@@ -1,9 +1,6 @@
'use client';
import { PopoverContent, PopoverTrigger } from '@radix-ui/react-popover';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { useSearchValue } from '@/hooks/use-search-value';
import { Popover } from '../ui/popover';
import type { Label } from '@/types';
import { cn } from '@/lib/utils';
import * as React from 'react';
@@ -41,7 +38,7 @@ export const RenderLabels = ({ count = 1, labels }: { count?: number; labels: La
key={label.id}
onClick={handleFilterByLabel(label)}
className={cn(
'dark:bg-subtleBlack bg-subtleWhite text-primary inline-block truncate rounded border px-1.5 py-0.5 text-xs font-medium overflow-hidden',
'dark:bg-subtleBlack bg-subtleWhite text-primary inline-block overflow-hidden truncate rounded border px-1.5 py-0.5 text-xs font-medium',
searchValue.value.includes(`label:${label.name}`) &&
'border-neutral-800 dark:border-white',
)}

View File

@@ -1,5 +1,3 @@
'use client';
import { useEmailAliases } from '@/hooks/use-email-aliases';
import { EmailComposer } from '../create/email-composer';
import { useHotkeysContext } from 'react-hotkeys-hook';
@@ -10,8 +8,8 @@ import { useThread } from '@/hooks/use-threads';
import { useSession } from '@/lib/auth-client';
import { serializeFiles } from '@/lib/schemas';
import { useDraft } from '@/hooks/use-drafts';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { useTranslations } from 'use-intl';
import type { Sender } from '@/types';
import { useQueryState } from 'nuqs';
import { toast } from 'sonner';

View File

@@ -3,7 +3,7 @@ import { useSearchValue } from '@/hooks/use-search-value';
import { useState, useEffect, useCallback } from 'react';
import { type DateRange } from 'react-day-picker';
import { Input } from '@/components/ui/input';
import { usePathname } from 'next/navigation';
import { useLocation } from 'react-router';
import { useForm } from 'react-hook-form';
import { Search, X } from 'lucide-react';
import { format } from 'date-fns';
@@ -29,7 +29,8 @@ export function SearchBar() {
// const [popoverOpen, setPopoverOpen] = useState(false);
const [, setSearchValue] = useSearchValue();
const [isSearching, setIsSearching] = useState(false);
const pathname = usePathname();
const location = useLocation();
const pathname = location.pathname;
const form = useForm<SearchForm>({
defaultValues: {

Some files were not shown because too many files have changed in this diff Show More