forked from nextjs/saas-starter
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 61195ab
Showing
37 changed files
with
4,468 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return 'Hello'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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('/'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" />; | ||
} |
Oops, something went wrong.