generated from muhtadeetaron/nextjs-template
initial commit
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user