Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ docs
.DS_Store
.vscode
.env
.claude/settings.local.json
.claude/
.pnp.*
.yarn/*
!.yarn/patches
Expand Down
5 changes: 4 additions & 1 deletion next/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ ENVIRONMENT=development
PORT=3000

NEXT_PUBLIC_API_URL=http://localhost:1337
PREVIEW_SECRET=preview_secret
PREVIEW_SECRET=preview_secret

# Must match BETTER_AUTH_SECRET in strapi/.env
BETTER_AUTH_SECRET=tobemodified
11 changes: 11 additions & 0 deletions next/app/[locale]/sign-in/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AmbientColor } from '@/components/decorations/ambient-color';
import { SignInForm } from '@/components/sign-in-form';

export default function SignInPage() {
return (
<div className="relative overflow-hidden">
<AmbientColor />
<SignInForm />
</div>
);
}
31 changes: 19 additions & 12 deletions next/components/navbar/desktop-navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import { useState } from 'react';

import { LocaleSwitcher } from '../locale-switcher';
import { NavbarItem } from './navbar-item';
import { UserMenu } from './user-menu';
import { Button } from '@/components/elements/button';
import { Logo } from '@/components/logo';
import { useSession } from '@/lib/auth-client';
import { cn } from '@/lib/utils';

type Props = {
Expand All @@ -37,6 +39,7 @@ export const DesktopNavbar = ({
locale,
}: Props) => {
const { scrollY } = useScroll();
const { data: session } = useSession();

const [showBackground, setShowBackground] = useState(false);

Expand Down Expand Up @@ -90,18 +93,22 @@ export const DesktopNavbar = ({
<div className="flex space-x-2 items-center">
<LocaleSwitcher currentLocale={locale} />

{rightNavbarItems.map((item, index) => (
<Button
key={item.text}
variant={
index === rightNavbarItems.length - 1 ? 'primary' : 'simple'
}
as={Link}
href={`/${locale}${item.URL}`}
>
{item.text}
</Button>
))}
{session?.user ? (
<UserMenu locale={locale} />
) : (
rightNavbarItems.map((item, index) => (
<Button
key={item.text}
variant={
index === rightNavbarItems.length - 1 ? 'primary' : 'simple'
}
as={Link}
href={`/${locale}${item.URL}`}
>
{item.text}
</Button>
))
)}
</div>
</motion.div>
);
Expand Down
34 changes: 20 additions & 14 deletions next/components/navbar/mobile-navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import { useMotionValueEvent, useScroll } from 'framer-motion';
import { Link } from 'next-view-transitions';
import { useState } from 'react';
import { IoIosMenu } from 'react-icons/io';
import { IoIosClose } from 'react-icons/io';
import { IoIosMenu, IoIosClose } from 'react-icons/io';

import { LocaleSwitcher } from '../locale-switcher';
import { UserMenu } from './user-menu';
import { Button } from '@/components/elements/button';
import { Logo } from '@/components/logo';
import { useSession } from '@/lib/auth-client';
import { cn } from '@/lib/utils';

type Props = {
Expand All @@ -33,6 +34,7 @@ export const MobileNavbar = ({
locale,
}: Props) => {
const [open, setOpen] = useState(false);
const { data: session } = useSession();

const { scrollY } = useScroll();

Expand Down Expand Up @@ -109,18 +111,22 @@ export const MobileNavbar = ({
))}
</div>
<div className="flex flex-row w-full items-start gap-2.5 px-8 py-4 ">
{rightNavbarItems.map((item, index) => (
<Button
key={item.text}
variant={
index === rightNavbarItems.length - 1 ? 'primary' : 'simple'
}
as={Link}
href={`/${locale}${item.URL}`}
>
{item.text}
</Button>
))}
{session?.user ? (
<UserMenu locale={locale} />
) : (
rightNavbarItems.map((item, index) => (
<Button
key={item.text}
variant={
index === rightNavbarItems.length - 1 ? 'primary' : 'simple'
}
as={Link}
href={`/${locale}${item.URL}`}
>
{item.text}
</Button>
))
)}
</div>
</div>
)}
Expand Down
42 changes: 42 additions & 0 deletions next/components/navbar/user-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client';

import { useRouter } from 'next/navigation';
import { useState } from 'react';

import { Button } from '@/components/elements/button';
import { signOut, useSession } from '@/lib/auth-client';

export const UserMenu = ({ locale }: { locale: string }) => {
const router = useRouter();
const { data: session, isPending } = useSession();
const [isSigningOut, setIsSigningOut] = useState(false);

if (isPending) return null;

if (!session?.user) return null;

const displayName = session.user.name || session.user.email;

async function handleSignOut() {
setIsSigningOut(true);
await signOut();
setIsSigningOut(false);
router.push(`/${locale}`);
router.refresh();
}

return (
<div className="flex items-center gap-2">
<span className="text-white text-sm whitespace-nowrap">
Hi {displayName}
</span>
<Button
variant="simple"
onClick={handleSignOut}
disabled={isSigningOut}
>
{isSigningOut ? 'Logging out…' : 'Logout'}
</Button>
</div>
);
};
97 changes: 91 additions & 6 deletions next/components/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,129 @@ import {
IconBrandGithubFilled,
IconBrandGoogleFilled,
} from '@tabler/icons-react';
import React from 'react';
import { Link } from 'next-view-transitions';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';

import { Container } from './container';
import { Button } from './elements/button';
import { Logo } from './logo';
import { signIn, signUp } from '@/lib/auth-client';

export const Register = () => {
const router = useRouter();
const params = useParams<{ locale: string }>();
const locale = params?.locale ?? 'en';
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);

async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
setIsSubmitting(true);

const { error: signUpError } = await signUp.email({
email,
password,
name: name || email,
});

setIsSubmitting(false);

if (signUpError) {
setError(signUpError.message ?? 'Sign up failed');
return;
}

router.push('/');
router.refresh();
}

async function handleSocial(provider: 'github' | 'google') {
setError(null);
const { error: socialError } = await signIn.social({
provider,
callbackURL: `/${locale}`,
});
if (socialError) {
setError(socialError.message ?? `${provider} sign-in failed`);
}
}

return (
<Container className="h-screen max-w-lg mx-auto flex flex-col items-center justify-center">
<Logo />
<h1 className="text-xl md:text-4xl font-bold my-4">
Sign up for LaunchPad
</h1>

<form className="w-full my-4">
<form className="w-full my-4" onSubmit={handleSubmit}>
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
className="h-10 pl-4 w-full mb-4 rounded-md text-sm bg-charcoal border border-neutral-800 text-white placeholder-neutral-500 outline-none focus:outline-none active:outline-none focus:ring-2 focus:ring-neutral-800"
/>
<input
type="email"
placeholder="Email Address"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="h-10 pl-4 w-full mb-4 rounded-md text-sm bg-charcoal border border-neutral-800 text-white placeholder-neutral-500 outline-none focus:outline-none active:outline-none focus:ring-2 focus:ring-neutral-800"
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
className="h-10 pl-4 w-full mb-4 rounded-md text-sm bg-charcoal border border-neutral-800 text-white placeholder-neutral-500 outline-none focus:outline-none active:outline-none focus:ring-2 focus:ring-neutral-800"
/>
<Button variant="muted" type="submit" className="w-full py-3">
<span className="text-sm">Sign up</span>
{error && (
<p className="text-sm text-red-400 mb-4">{error}</p>
)}
<Button
variant="muted"
type="submit"
className="w-full py-3"
disabled={isSubmitting}
>
<span className="text-sm">{isSubmitting ? 'Signing up…' : 'Sign up'}</span>
</Button>
</form>

<p className="text-sm text-neutral-400">
Already have an account?{' '}
<Link
href={`/${locale}/sign-in`}
className="text-white underline underline-offset-2 hover:text-secondary"
>
Sign in
</Link>
</p>

<Divider />

<div className="flex flex-col sm:flex-row gap-4 w-full">
<button className="flex flex-1 justify-center space-x-2 items-center bg-white px-4 py-3 rounded-md text-black hover:bg-white/80 transition duration-200 shadow-[0px_1px_0px_0px_#00000040_inset]">
<button
type="button"
onClick={() => handleSocial('github')}
className="flex flex-1 justify-center space-x-2 items-center bg-white px-4 py-3 rounded-md text-black hover:bg-white/80 transition duration-200 shadow-[0px_1px_0px_0px_#00000040_inset]"
>
<IconBrandGithubFilled className="h-4 w-4 text-black" />
<span className="text-sm">Login with GitHub</span>
</button>
<button className="flex flex-1 justify-center space-x-2 items-center bg-white px-4 py-3 rounded-md text-black hover:bg-white/80 transition duration-200 shadow-[0px_1px_0px_0px_#00000040_inset]">
<button
type="button"
onClick={() => handleSocial('google')}
className="flex flex-1 justify-center space-x-2 items-center bg-white px-4 py-3 rounded-md text-black hover:bg-white/80 transition duration-200 shadow-[0px_1px_0px_0px_#00000040_inset]"
>
<IconBrandGoogleFilled className="h-4 w-4 text-black" />
<span className="text-sm">Login with Google</span>
</button>
Expand Down
Loading