Skip to content

Commit

Permalink
Working app
Browse files Browse the repository at this point in the history
  • Loading branch information
leerob committed Sep 9, 2024
0 parents commit 61195ab
Show file tree
Hide file tree
Showing 37 changed files with 4,468 additions and 0 deletions.
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 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*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
.vscode
44 changes: 44 additions & 0 deletions app/(dashboard)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client';

import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { ArrowRight } from 'lucide-react';
import { useUser } from '@/lib/auth';

export default function DashboardPage() {
let user = useUser();

return (
<main className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h1 className="text-3xl font-medium text-gray-900 mb-8">
Dashboard Tutorial
</h1>
<ol className="list-decimal list-inside space-y-6 text-gray-700">
<li className="pl-2">
Welcome to the dashboard. This route is protected and only allowed to
be accessed when logged in. Your username is{' '}
<strong className="text-gray-900">{user?.username}</strong>.
</li>
<li className="pl-2">
Learn more about how this route is protected by{' '}
<Link href="#" className="text-blue-600 hover:underline">
exploring the code
</Link>
.
</li>
<li className="pl-2">
<Link href="#" className="text-blue-600 hover:underline">
Clone and deploy
</Link>{' '}
your own version to get started building your SaaS.
</li>
</ol>
<div className="mt-12">
<Button className="w-full bg-white hover:bg-gray-100 text-black border border-gray-200 rounded-full flex items-center justify-center">
Manage Subscription
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</main>
);
}
10 changes: 10 additions & 0 deletions app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Header } from '@/app/header';

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<section>
<Header />
{children}
</section>
);
}
3 changes: 3 additions & 0 deletions app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return 'Hello';
}
72 changes: 72 additions & 0 deletions app/(dashboard)/pricing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Button } from '@/components/ui/button';
import { ArrowRight, Check } from 'lucide-react';

export default function PricingPage() {
return (
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="grid md:grid-cols-2 gap-8 max-w-xl mx-auto">
<div className="pt-6">
<h2 className="text-2xl font-medium text-gray-900 mb-2">Base</h2>
<p className="text-sm text-gray-600 mb-4">with 7 day free trial</p>
<p className="text-4xl font-medium text-gray-900 mb-6">
$8{' '}
<span className="text-xl font-normal text-gray-600">
per user / month
</span>
</p>
<ul className="space-y-4 mb-8">
<li className="flex items-start">
<Check className="h-5 w-5 text-orange-500 mr-2 mt-0.5 flex-shrink-0" />
<span className="text-gray-700">Unlimited Emails</span>
</li>
<li className="flex items-start">
<Check className="h-5 w-5 text-orange-500 mr-2 mt-0.5 flex-shrink-0" />
<span className="text-gray-700">
Unlimited Attachment Storage
</span>
</li>
<li className="flex items-start">
<Check className="h-5 w-5 text-orange-500 mr-2 mt-0.5 flex-shrink-0" />
<span className="text-gray-700">Unlimited Workspace Members</span>
</li>
</ul>
<Button className="w-full bg-white hover:bg-gray-100 text-black border border-gray-200 rounded-full flex items-center justify-center">
Get Started
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>

<div className="pt-6">
<h2 className="text-2xl font-medium text-gray-900 mb-2">Plus</h2>
<p className="text-sm text-gray-600 mb-4">with 7 day free trial</p>
<p className="text-4xl font-medium text-gray-900 mb-6">
$12{' '}
<span className="text-xl font-normal text-gray-600">
per user / month
</span>
</p>
<ul className="space-y-4 mb-8">
<li className="flex items-start">
<Check className="h-5 w-5 text-orange-500 mr-2 mt-0.5 flex-shrink-0" />
<span className="text-gray-700">Everything in Base, and:</span>
</li>
<li className="flex items-start">
<Check className="h-5 w-5 text-orange-500 mr-2 mt-0.5 flex-shrink-0" />
<span className="text-gray-700">
Early Access to New Features
</span>
</li>
<li className="flex items-start">
<Check className="h-5 w-5 text-orange-500 mr-2 mt-0.5 flex-shrink-0" />
<span className="text-gray-700">Recognition in Our Credits</span>
</li>
</ul>
<Button className="w-full bg-white hover:bg-gray-100 text-black border border-gray-200 rounded-full flex items-center justify-center">
Get Started
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
</main>
);
}
91 changes: 91 additions & 0 deletions app/(login)/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use server';

import { z } from 'zod';
import { eq } from 'drizzle-orm';
import { db } from '@/lib/db/drizzle';
import { users, type NewUser } from '@/lib/db/schema';
import { comparePasswords, hashPassword, setSession } from '@/lib/auth/session';
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
import { revalidatePath } from 'next/cache';

const userSchema = z.object({
username: z.string().min(3).max(50),
password: z.string().min(8).max(100),
});

export async function signIn(_: any, formData: FormData) {
const result = userSchema.safeParse({
username: formData.get('username'),
password: formData.get('password'),
});

if (!result.success) {
return { error: 'Invalid input. Please check your username and password.' };
}

const { username, password } = result.data;

const user = await db
.select()
.from(users)
.where(eq(users.username, username))
.limit(1);

if (user.length === 0) {
return { error: 'User not found. Please try again.' };
}

const isPasswordValid = await comparePasswords(
password,
user[0].passwordHash,
);

if (!isPasswordValid) {
return { error: 'Incorrect password. Please try again.' };
}

setSession(user[0]);
redirect('/dashboard');
}

export async function signUp(_: any, formData: FormData) {
const result = userSchema.safeParse({
username: formData.get('username'),
password: formData.get('password'),
});

if (!result.success) {
return { error: 'Invalid input. Please check your username and password.' };
}

const { username, password } = result.data;

const existingUser = await db
.select()
.from(users)
.where(eq(users.username, username))
.limit(1);

if (existingUser.length > 0) {
return { error: 'Username already exists.' };
}

const passwordHash = await hashPassword(password);

const newUser: NewUser = {
username,
passwordHash,
};

await db.insert(users).values(newUser);

setSession(existingUser[0]);
redirect('/dashboard');
}

export async function signOut() {
cookies().set('session', '', { expires: new Date(0) });
revalidatePath('/', 'layout');
redirect('/');
}
130 changes: 130 additions & 0 deletions app/(login)/login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use client';

import { useFormState as useActionState } from 'react-dom';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { CircleIcon, Loader2 } from 'lucide-react';
import { signIn, signUp } from './actions';

export function Login({ mode = 'signin' }: { mode?: 'signin' | 'signup' }) {
const [state, formAction, pending] = useActionState(
mode === 'signin' ? signIn : signUp,
{ error: '' },
);

return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<div className="flex justify-center">
<CircleIcon className="h-12 w-12 text-orange-500" />
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
{mode === 'signin'
? 'Sign in to your account'
: 'Create your account'}
</h2>
</div>

<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<form className="space-y-6" action={formAction}>
<div>
<Label
htmlFor="username"
className="block text-sm font-medium text-gray-700"
>
Username
</Label>
<div className="mt-1">
<Input
id="username"
name="username"
type="text"
autoComplete="username"
required
minLength={3}
maxLength={50}
className="appearance-none rounded-full relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-orange-500 focus:border-orange-500 focus:z-10 sm:text-sm"
placeholder="Enter your username"
/>
</div>
</div>

<div>
<Label
htmlFor="password"
className="block text-sm font-medium text-gray-700"
>
Password
</Label>
<div className="mt-1">
<Input
id="password"
name="password"
type="password"
autoComplete={
mode === 'signin' ? 'current-password' : 'new-password'
}
required
minLength={8}
maxLength={100}
className="appearance-none rounded-full relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-orange-500 focus:border-orange-500 focus:z-10 sm:text-sm"
placeholder="Enter your password"
/>
</div>
</div>

{state.error && (
<div className="text-red-500 text-sm">{state.error}</div>
)}

<div>
<Button
type="submit"
className="w-full flex justify-center items-center py-2 px-4 border border-transparent rounded-full shadow-sm text-sm font-medium text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500"
disabled={pending}
>
{pending ? (
<>
<Loader2 className="animate-spin mr-2 h-4 w-4" />
Loading...
</>
) : mode === 'signin' ? (
'Sign in'
) : (
'Sign up'
)}
</Button>
</div>
</form>

<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-gray-50 text-gray-500">
{mode === 'signin'
? 'New to our platform?'
: 'Already have an account?'}
</span>
</div>
</div>

<div className="mt-6">
<Link
href={mode === 'signin' ? '/sign-up' : '/sign-in'}
className="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-full shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500"
>
{mode === 'signin'
? 'Create an account'
: 'Sign in to existing account'}
</Link>
</div>
</div>
</div>
</div>
);
}
5 changes: 5 additions & 0 deletions app/(login)/sign-in/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Login } from '../login';

export default function SignInPage() {
return <Login mode="signin" />;
}
Loading

0 comments on commit 61195ab

Please sign in to comment.