generated from muhtadeetaron/nextjs-template
initial commit
This commit is contained in:
3
torpedo/.eslintrc.json
Normal file
3
torpedo/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
36
torpedo/.gitignore
vendored
Normal file
36
torpedo/.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
36
torpedo/README.md
Normal file
36
torpedo/README.md
Normal file
@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
36
torpedo/app/api/auth/[...nextauth]/route.ts
Normal file
36
torpedo/app/api/auth/[...nextauth]/route.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import NextAuth, { NextAuthOptions } from "next-auth";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
import { JWT } from "next-auth/jwt";
|
||||
import { Session } from "next-auth";
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||
authorization: {
|
||||
params: {
|
||||
scope:
|
||||
"openid profile email https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/spreadsheets.readonly",
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
callbacks: {
|
||||
async session({ session, token }: { session: Session; token: JWT }) {
|
||||
session.accessToken = token.accessToken as string;
|
||||
return session;
|
||||
},
|
||||
async jwt({ token, account }: { token: JWT; account: any }) {
|
||||
if (account) {
|
||||
token.accessToken = account.access_token;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
42
torpedo/app/api/fetchcsv/route.ts
Normal file
42
torpedo/app/api/fetchcsv/route.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { google } from 'googleapis';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '../auth/[...nextauth]/route';
|
||||
|
||||
async function listCSVFileNames(accessToken: string): Promise<string[]> {
|
||||
const auth = new google.auth.OAuth2();
|
||||
console.log(accessToken)
|
||||
auth.setCredentials({ access_token: accessToken });
|
||||
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
|
||||
try {
|
||||
const res = await drive.files.list({
|
||||
q: "mimeType='text/csv'",
|
||||
fields: 'files(name)',
|
||||
});
|
||||
|
||||
const fileNames = res.data.files?.map(file => file.name) || [];
|
||||
|
||||
return fileNames
|
||||
} catch (error) {
|
||||
console.error('Error listing CSV files:', error);
|
||||
throw new Error('Failed to list CSV files');
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getServerSession({ req: request, authOptions });
|
||||
|
||||
|
||||
if (!session || !session.accessToken) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const fileNames = await listCSVFileNames(session.accessToken);
|
||||
return NextResponse.json({ fileNames });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
62
torpedo/app/auth/login/page.tsx
Normal file
62
torpedo/app/auth/login/page.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
import { Inter } from "next/font/google";
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
import { signIn } from "next-auth/react"; // Import signIn function
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<main className="flex bg-[#151515] h-screen">
|
||||
<section className="w-1/2 flex items-end overflow-hidden">
|
||||
<Image
|
||||
src="/image/splash.png"
|
||||
alt="login splash"
|
||||
width={1000}
|
||||
height={1000}
|
||||
/>
|
||||
</section>
|
||||
<section className="w-1/2 text-white flex items-center justify-center">
|
||||
<div className="h-[700px] w-[500px] flex flex-col justify-between">
|
||||
<section>
|
||||
<Link href="/">
|
||||
<Image
|
||||
src="/logo/logo-full.svg"
|
||||
alt="logo"
|
||||
width={200}
|
||||
height={1000}
|
||||
/>
|
||||
</Link>
|
||||
</section>
|
||||
<section className="flex flex-col justify-between h-1/3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1
|
||||
className={`text-white text-5xl ${inter.className} font-bold tracking-tighter`}
|
||||
>
|
||||
Hi There 👋
|
||||
</h1>
|
||||
<p className="text-xl tracking-tighter">
|
||||
Login to your Google account.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-[#4285F4] h-[65px] text-lg tracking-tight"
|
||||
onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
|
||||
>
|
||||
Login with Google
|
||||
</Button>
|
||||
</section>
|
||||
<section className="text-[#efefef]/50 tracking-tighter">
|
||||
<p>© 2024 Torpedo</p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
7
torpedo/app/dashboard/contacts/page.tsx
Normal file
7
torpedo/app/dashboard/contacts/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>Contacts</div>;
|
||||
};
|
||||
|
||||
export default page;
|
||||
174
torpedo/app/dashboard/email/page.tsx
Normal file
174
torpedo/app/dashboard/email/page.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
"use client";
|
||||
|
||||
import { Avatar } from "@/components/ui/avatar";
|
||||
import { AvatarImage } from "@radix-ui/react-avatar";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { ContactSelector } from "@/components/dashboard/email/ContactSelector";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import RichTextEditor from "@/components/dashboard/email/RichTextEditor";
|
||||
import { Eye, Send, FileBox, Check } from "lucide-react";
|
||||
import ContactModal from "@/components/dashboard/email/ContactModal";
|
||||
|
||||
const page = () => {
|
||||
const { data: session } = useSession();
|
||||
|
||||
return (
|
||||
<main className="px-4 py-6 border-white h-full flex flex-col gap-6">
|
||||
<header>
|
||||
<h1 className="font-bold text-5xl tracking-tighter text-white">
|
||||
Send an Email
|
||||
</h1>
|
||||
</header>
|
||||
<section className="flex gap-4">
|
||||
<section className="w-1/3 h-fit flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>From</Label>
|
||||
<div className="flex justify-between w-full rounded-xl py-2 pl-2 pr-4 border-[1px] border-[#CBD5E1]">
|
||||
<div className="rounded-xl px-3 py-2 flex items-center gap-2 bg-[#3D3D3D] w-fit">
|
||||
<Avatar className="h-7 w-7">
|
||||
<AvatarImage src={session?.user?.image!} />
|
||||
</Avatar>
|
||||
<h2 className="tracking-tight text-white text-sm font-semibold">
|
||||
{session?.user?.email}
|
||||
</h2>
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Image
|
||||
src="/icons/gmail.svg"
|
||||
alt="gmail logo"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex flex-col items-center font-semibold">
|
||||
Currently logged in as:{" "}
|
||||
<p className="font-normal"> {session?.user?.email}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>To</Label>
|
||||
<div className="flex items-center justify-between w-full rounded-xl p-2 border-[1px] border-[#CBD5E1]">
|
||||
<ContactSelector />
|
||||
<ContactModal />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="subject">Subject</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="subject"
|
||||
placeholder="What's the email about?"
|
||||
className="w-full rounded-xl py-7 px-4 border-[1px] border-[#CBD5E1] bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="subject">Attachments</Label>
|
||||
<div className="h-96 border-[1px] border-[#CBD5E1] rounded-xl"></div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="w-2/3 flex flex-col gap-2">
|
||||
<Label>Body</Label>
|
||||
<RichTextEditor />
|
||||
</section>
|
||||
</section>
|
||||
<section className="flex justify-between">
|
||||
<div></div>
|
||||
<div className="flex gap-4">
|
||||
<Sheet>
|
||||
<SheetTrigger className="h-auto flex gap-2 justify-center rounded-lg text-sm items-center text-white w-40 bg-[#363636]">
|
||||
<Eye size={18} />
|
||||
Preview
|
||||
</SheetTrigger>
|
||||
<SheetContent className="bg-[#222222] flex flex-col gap-10">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-4xl tracking-tighter">
|
||||
Preview
|
||||
</SheetTitle>
|
||||
<SheetDescription>
|
||||
This is what your email looks like.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<section className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>From</Label>
|
||||
<div className="flex justify-between w-full rounded-xl py-2 pl-2 pr-4 border-[1px] border-[#CBD5E1]">
|
||||
<div className="rounded-xl px-3 py-2 flex items-center gap-2 w-fit">
|
||||
<h2 className="tracking-tight text-white text-sm ">
|
||||
{session?.user?.email}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>To</Label>
|
||||
<div className="flex justify-between w-full rounded-xl py-2 pl-2 pr-4 border-[1px] border-[#CBD5E1]">
|
||||
<div className="rounded-xl px-3 py-2 flex items-center gap-2 w-fit">
|
||||
<h2 className="tracking-tight text-white text-sm ">
|
||||
alex.mason@gmail.com
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Subject</Label>
|
||||
<div className="flex justify-between w-full rounded-xl py-2 pl-2 pr-4 border-[1px] border-[#CBD5E1]">
|
||||
<div className="rounded-xl px-3 py-2 flex items-center gap-2 w-fit">
|
||||
<h2 className="tracking-tight text-white text-sm ">
|
||||
Greetings for the new year!
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Body</Label>
|
||||
<div className="flex justify-between w-full rounded-xl py-2 pl-2 pr-4 border-[1px] border-[#CBD5E1]">
|
||||
<div className="rounded-xl px-3 py-2 flex items-center gap-2 w-fit">
|
||||
<p className=" text-white text-sm">
|
||||
Hello, Alex Merry Christmas and a Happy New Year to you
|
||||
all Engineers. I’m glad to announce that we will be
|
||||
arranging a Christmas party on the eve of Christmas. As
|
||||
such, you are cordially invited to attend the party as
|
||||
we will have loads of fun events like Dart, Mario Kart
|
||||
and Beer Pong. Hope to see you at the party! Regards, HR
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<Button className="flex gap-2 w-40 bg-[#2477FF] text-white">
|
||||
<Send size={18} />
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
20
torpedo/app/dashboard/layout.tsx
Normal file
20
torpedo/app/dashboard/layout.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
// app/dashboard/layout.tsx
|
||||
"use client";
|
||||
import Sidebar from "@/components/dashboard/Sidebar";
|
||||
import React from "react";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
|
||||
const DashboardLayout: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<SessionProvider>
|
||||
<Sidebar />
|
||||
<main className="flex-1 p-6 bg-[#151515]">{children}</main>
|
||||
</SessionProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardLayout;
|
||||
13
torpedo/app/dashboard/page.tsx
Normal file
13
torpedo/app/dashboard/page.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<>
|
||||
<h1>Dashboard</h1>
|
||||
<p>Welcome to your dashboard!</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
7
torpedo/app/dashboard/settings/page.tsx
Normal file
7
torpedo/app/dashboard/settings/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>Settings</div>;
|
||||
};
|
||||
|
||||
export default page;
|
||||
76
torpedo/app/globals.css
Normal file
76
torpedo/app/globals.css
Normal file
@ -0,0 +1,76 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
padding: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
min-height: 70vh
|
||||
}
|
||||
32
torpedo/app/layout.tsx
Normal file
32
torpedo/app/layout.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Torpedo",
|
||||
description: "Torpedo bulk email app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
84
torpedo/app/page.tsx
Normal file
84
torpedo/app/page.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
import { useRef, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
interface Position {
|
||||
left: number;
|
||||
width: number;
|
||||
opacity: number;
|
||||
}
|
||||
interface TabProps {
|
||||
children: React.ReactNode;
|
||||
setPosition: (position: Position) => void;
|
||||
}
|
||||
const Slidetabs: React.FC = () => {
|
||||
const [position, setPosition] = useState<Position>({
|
||||
left: 60,
|
||||
width: 150,
|
||||
opacity: 1,
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="relative mx-auto flex w-fit rounded-full border-2 border-black bg-white p-1">
|
||||
<Tab setPosition={setPosition}>Home</Tab>
|
||||
<Tab setPosition={setPosition}>Features</Tab>
|
||||
<Tab setPosition={setPosition}>Pricing</Tab>
|
||||
<Tab setPosition={setPosition}>Docs</Tab>
|
||||
<Tab setPosition={setPosition}>Blog</Tab>
|
||||
<Cursor position={position} />
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
const Tab: React.FC<TabProps> = ({ children, setPosition }) => {
|
||||
const ref = useRef<HTMLLIElement | null>(null);
|
||||
|
||||
return (
|
||||
<li
|
||||
ref={ref}
|
||||
onMouseEnter={() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const { width } = ref.current.getBoundingClientRect();
|
||||
setPosition({
|
||||
width,
|
||||
opacity: 1,
|
||||
left: ref.current.offsetLeft,
|
||||
});
|
||||
}}
|
||||
className="relative z-10 block cursor-pointer px-3 py-1.5 text-xs uppercase text-white mix-blend-difference md:px-5 md:py-3 md:text-base"
|
||||
>
|
||||
{children}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
const Cursor: React.FC<{ position: Position }> = ({ position }) => {
|
||||
return (
|
||||
<motion.li
|
||||
animate={position}
|
||||
className="absolute z-0 w-36 h-7 rounded-full bg-black md:h-12"
|
||||
></motion.li>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<body className="">
|
||||
<header className="flex justify-between">
|
||||
<div className="">
|
||||
<Image src="/logo/logo-full.svg" alt="logo" width={200} height={0} />
|
||||
</div>
|
||||
<nav>
|
||||
<Slidetabs />
|
||||
</nav>
|
||||
<div>
|
||||
<Button>Login</Button>
|
||||
</div>
|
||||
</header>
|
||||
<main className="text-7xl">Welcome to Torpedo</main>
|
||||
</body>
|
||||
);
|
||||
}
|
||||
BIN
torpedo/bun.lockb
Normal file
BIN
torpedo/bun.lockb
Normal file
Binary file not shown.
17
torpedo/components.json
Normal file
17
torpedo/components.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
190
torpedo/components/dashboard/Sidebar.tsx
Normal file
190
torpedo/components/dashboard/Sidebar.tsx
Normal file
File diff suppressed because one or more lines are too long
111
torpedo/components/dashboard/email/ContactModal.tsx
Normal file
111
torpedo/components/dashboard/email/ContactModal.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Eye, Send, FileBox, Check, CircleUser } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type CsvFile = {
|
||||
name: string;
|
||||
csvLink: string;
|
||||
};
|
||||
|
||||
const ContactModal = () => {
|
||||
const [csvFiles, setCsvFiles] = useState<CsvFile[]>([]);
|
||||
console.log(csvFiles);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCsvFiles = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/fetchcsv");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch CSV files");
|
||||
}
|
||||
const data: CsvFile[] = await response.json();
|
||||
setCsvFiles(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching CSV files:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCsvFiles();
|
||||
}, []); // Only run on mount
|
||||
return (
|
||||
<div>
|
||||
<Dialog>
|
||||
<DialogTrigger className="px-4 bg-white text-black py-2 rounded-md">
|
||||
<FileBox size={20} />
|
||||
</DialogTrigger>
|
||||
<DialogContent className="h-5/6 w-5/6 bg-[#222222]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-4xl tracking-tighter">
|
||||
Select Contacts
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select a file to import contacts from.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<section className="flex gap-2">
|
||||
<Select>
|
||||
<SelectTrigger className="w-1/6 rounded-xl">
|
||||
<SelectValue placeholder="Select Option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="gsheets">
|
||||
<div className="flex items-center gap-1">
|
||||
<Image
|
||||
src="/icons/Sheets.svg"
|
||||
alt="Google Sheets Icon"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Google Sheets
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="system">
|
||||
<div className="flex items-center gap-1">
|
||||
<CircleUser size={20} />
|
||||
Contact Manager
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select>
|
||||
<SelectTrigger className="w-4/6 rounded-xl">
|
||||
<SelectValue placeholder="Select File (.csv, .tsv)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{csvFiles.map((file) => (
|
||||
<SelectItem key={file.name} value={file.name}>
|
||||
{file.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button className="bg-[#34A853] h-9 rounded-xl w-1/6 text-white flex gap-2">
|
||||
<Check />
|
||||
Select
|
||||
</Button>
|
||||
</section>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactModal;
|
||||
94
torpedo/components/dashboard/email/ContactSelector.tsx
Normal file
94
torpedo/components/dashboard/email/ContactSelector.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
const attributes = [
|
||||
{
|
||||
value: "name",
|
||||
label: "name",
|
||||
},
|
||||
{
|
||||
value: "phone",
|
||||
label: "phone",
|
||||
},
|
||||
{
|
||||
value: "email",
|
||||
label: "email",
|
||||
},
|
||||
{
|
||||
value: "id",
|
||||
label: "id",
|
||||
},
|
||||
{
|
||||
value: "address",
|
||||
label: "address",
|
||||
},
|
||||
];
|
||||
|
||||
export function ContactSelector() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [value, setValue] = React.useState("");
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-[200px] justify-between bg-[#3d3d3d]"
|
||||
>
|
||||
{value
|
||||
? attributes.find((attribute) => attribute.value === value)?.label
|
||||
: "Select email field..."}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0 ">
|
||||
<Command className="bg-[#222222]">
|
||||
<CommandInput placeholder="Search attribute..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No attribute found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{attributes.map((attribute) => (
|
||||
<CommandItem
|
||||
key={attribute.value}
|
||||
value={attribute.value}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? "" : currentValue);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
value === attribute.value ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{attribute.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
87
torpedo/components/dashboard/email/RichTextEditor.tsx
Normal file
87
torpedo/components/dashboard/email/RichTextEditor.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
// components/RichTextEditor.tsx
|
||||
import { useEditor, EditorContent, Editor } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Heading from "@tiptap/extension-heading";
|
||||
import Color from "@tiptap/extension-color";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
Strikethrough,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
|
||||
const MenuBar: React.FC<{ editor: Editor | null }> = ({ editor }) => {
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="toolbar pb-3 flex gap-2">
|
||||
<Toggle onClick={() => editor.chain().focus().toggleBold().run()}>
|
||||
<Bold className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
className={editor.isActive("italic") ? "is-active" : ""}
|
||||
>
|
||||
<Italic className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
className={editor.isActive("strike") ? "is-active" : ""}
|
||||
>
|
||||
<Strikethrough className="h-4 w-4" />
|
||||
</Toggle>
|
||||
{/* Heading Toggles */}
|
||||
<Toggle
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
className={editor.isActive("heading", { level: 1 }) ? "is-active" : ""}
|
||||
>
|
||||
<Heading1 className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
className={editor.isActive("heading", { level: 2 }) ? "is-active" : ""}
|
||||
>
|
||||
<Heading2 className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||
className={editor.isActive("heading", { level: 3 }) ? "is-active" : ""}
|
||||
>
|
||||
<Heading3 className="h-4 w-4" />
|
||||
</Toggle>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RichTextEditor: React.FC = () => {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Heading.configure({
|
||||
levels: [1, 2, 3],
|
||||
}),
|
||||
Color,
|
||||
TextStyle,
|
||||
],
|
||||
content: "<p>Write your email here...</p>",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<MenuBar editor={editor} />
|
||||
<EditorContent
|
||||
className="flex-1 overflow-y-auto overflow-x-auto h-[70vh]"
|
||||
editor={editor}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RichTextEditor;
|
||||
9
torpedo/components/theme-provider.tsx
Normal file
9
torpedo/components/theme-provider.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
50
torpedo/components/ui/avatar.tsx
Normal file
50
torpedo/components/ui/avatar.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
57
torpedo/components/ui/button.tsx
Normal file
57
torpedo/components/ui/button.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
155
torpedo/components/ui/command.tsx
Normal file
155
torpedo/components/ui/command.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
122
torpedo/components/ui/dialog.tsx
Normal file
122
torpedo/components/ui/dialog.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
25
torpedo/components/ui/input.tsx
Normal file
25
torpedo/components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
26
torpedo/components/ui/label.tsx
Normal file
26
torpedo/components/ui/label.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
31
torpedo/components/ui/popover.tsx
Normal file
31
torpedo/components/ui/popover.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
160
torpedo/components/ui/select.tsx
Normal file
160
torpedo/components/ui/select.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
140
torpedo/components/ui/sheet.tsx
Normal file
140
torpedo/components/ui/sheet.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
46
torpedo/components/ui/toggle.tsx
Normal file
46
torpedo/components/ui/toggle.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-white hover:text-black focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-white data-[state=on]:text-black",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
custom: "bg-transparent hover:bg-white hover:text-black]",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3",
|
||||
sm: "h-9 px-2.5",
|
||||
lg: "h-11 px-5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName;
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
30
torpedo/components/ui/tooltip.tsx
Normal file
30
torpedo/components/ui/tooltip.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
6
torpedo/lib/utils.ts
Normal file
6
torpedo/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
22
torpedo/middleware.ts
Normal file
22
torpedo/middleware.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getToken } from 'next-auth/jwt';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
// Get the token from the request
|
||||
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
|
||||
|
||||
const { pathname } = req.nextUrl;
|
||||
|
||||
// If the user is not authenticated and trying to access the dashboard, redirect to login
|
||||
if (!token && pathname.startsWith('/dashboard')) {
|
||||
return NextResponse.redirect(new URL('/auth/login', req.url));
|
||||
}
|
||||
|
||||
// Allow the request to proceed
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/dashboard/:path*'], // Protect all routes under /dashboard
|
||||
};
|
||||
17
torpedo/next-auth.d.ts
vendored
Normal file
17
torpedo/next-auth.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
// next-auth.d.ts
|
||||
import NextAuth from "next-auth";
|
||||
import { JWT } from "next-auth/jwt";
|
||||
|
||||
// Extend the default session interface
|
||||
declare module "next-auth" {
|
||||
interface Session {
|
||||
accessToken?: string;
|
||||
}
|
||||
}
|
||||
|
||||
// Extend the default JWT interface
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
accessToken?: string;
|
||||
}
|
||||
}
|
||||
4
torpedo/next.config.mjs
Normal file
4
torpedo/next.config.mjs
Normal file
@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
52
torpedo/package.json
Normal file
52
torpedo/package.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "torpedo",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@tiptap/extension-color": "^2.6.4",
|
||||
"@tiptap/extension-heading": "^2.6.4",
|
||||
"@tiptap/extension-text-style": "^2.6.4",
|
||||
"@tiptap/react": "^2.6.4",
|
||||
"@tiptap/starter-kit": "^2.6.4",
|
||||
"@types/quill": "^2.0.14",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"framer-motion": "^11.3.29",
|
||||
"googleapis": "^142.0.0",
|
||||
"lucide-react": "^0.429.0",
|
||||
"next": "14.2.5",
|
||||
"next-auth": "^4.24.7",
|
||||
"next-themes": "^0.3.0",
|
||||
"quill": "^2.0.2",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"torpedo": "."
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.5"
|
||||
}
|
||||
}
|
||||
8
torpedo/postcss.config.mjs
Normal file
8
torpedo/postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
80
torpedo/tailwind.config.ts
Normal file
80
torpedo/tailwind.config.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import type { Config } from "tailwindcss"
|
||||
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
} satisfies Config
|
||||
|
||||
export default config
|
||||
26
torpedo/tsconfig.json
Normal file
26
torpedo/tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user