Cleaning up some code
This commit is contained in:
@ -1,132 +1,11 @@
|
||||
'use client';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from '@/components/ui';
|
||||
import Link from 'next/link';
|
||||
import { forgotPassword } from '@/lib/actions';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/components/context';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { StatusMessage, SubmitButton } from '@/components/default';
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: 'Please enter a valid email address.',
|
||||
}),
|
||||
});
|
||||
import { ForgotPasswordCard } from '@/components/default/auth';
|
||||
|
||||
const ForgotPassword = () => {
|
||||
const router = useRouter();
|
||||
const { isAuthenticated, isLoading, refreshUserData } = useAuth();
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
},
|
||||
});
|
||||
|
||||
// Redirect if already authenticated
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
router.push('/');
|
||||
}
|
||||
}, [isAuthenticated, router]);
|
||||
|
||||
const handleForgotPassword = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
setStatusMessage('');
|
||||
const formData = new FormData();
|
||||
formData.append('email', values.email);
|
||||
const result = await forgotPassword(formData);
|
||||
if (result?.success) {
|
||||
await refreshUserData();
|
||||
setStatusMessage(
|
||||
result?.data ?? 'Check your email for a link to reset your password.',
|
||||
);
|
||||
form.reset();
|
||||
router.push('');
|
||||
} else {
|
||||
setStatusMessage(`Error: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatusMessage(
|
||||
`Error: ${error instanceof Error ? error.message : 'Could not sign in!'}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<div className='w-full mx-auto text-center pt-2 md:pt-10'>
|
||||
<div className='mx-auto flex flex-col items-center justify-center'>
|
||||
<Card className='min-w-xs sm:min-w-sm sm:max-w-xs max-w-lg'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-2xl font-medium'>Reset Password</CardTitle>
|
||||
<CardDescription className='text-sm text-foreground'>
|
||||
Don't have an account?{' '}
|
||||
<Link className='font-medium underline' href='/sign-up'>
|
||||
Sign up
|
||||
</Link>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleForgotPassword)}
|
||||
className='flex flex-col min-w-64 space-y-6'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='email'
|
||||
placeholder='you@example.com'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={isLoading}
|
||||
pendingText='Resetting Password...'
|
||||
>
|
||||
Reset Password
|
||||
</SubmitButton>
|
||||
{statusMessage &&
|
||||
(statusMessage.includes('Error') ||
|
||||
statusMessage.includes('error') ||
|
||||
statusMessage.includes('failed') ||
|
||||
statusMessage.includes('invalid') ? (
|
||||
<StatusMessage message={{ error: statusMessage }} />
|
||||
) : (
|
||||
<StatusMessage message={{ success: statusMessage }} />
|
||||
))}
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ForgotPasswordCard />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,122 +1,14 @@
|
||||
'use client';
|
||||
import { useAuth } from '@/components/context';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
AvatarUpload,
|
||||
ProfileForm,
|
||||
ResetPasswordForm,
|
||||
SignOut,
|
||||
} from '@/components/default/profile';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
Separator,
|
||||
} from '@/components/ui';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { resetPassword } from '@/lib/actions';
|
||||
import { toast } from 'sonner';
|
||||
import { type Result } from '@/lib/actions';
|
||||
|
||||
import { ProfileCard } from "@/components/default/auth";
|
||||
|
||||
const ProfilePage = () => {
|
||||
const {
|
||||
profile,
|
||||
isLoading,
|
||||
isAuthenticated,
|
||||
updateProfile,
|
||||
refreshUserData,
|
||||
} = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isAuthenticated) {
|
||||
router.push('/sign-in');
|
||||
}
|
||||
}, [isLoading, isAuthenticated, router]);
|
||||
|
||||
const handleAvatarUploaded = async (path: string) => {
|
||||
await updateProfile({ avatar_url: path });
|
||||
await refreshUserData();
|
||||
};
|
||||
|
||||
const handleProfileSubmit = async (values: {
|
||||
full_name: string;
|
||||
email: string;
|
||||
}) => {
|
||||
try {
|
||||
await updateProfile({
|
||||
full_name: values.full_name,
|
||||
email: values.email,
|
||||
});
|
||||
} catch {
|
||||
toast.error('Error updating profile!: ');
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetPasswordSubmit = async (
|
||||
formData: FormData,
|
||||
): Promise<Result<null>> => {
|
||||
try {
|
||||
const result = await resetPassword(formData);
|
||||
if (!result.success) {
|
||||
toast.error(`Error resetting password: ${result.error}`);
|
||||
return { success: false, error: result.error };
|
||||
}
|
||||
return { success: true, data: null };
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Error resetting password!: ${(error as string) ?? 'Unknown error'}`,
|
||||
);
|
||||
return { success: false, error: 'Unknown error' };
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading state while checking authentication
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='flex justify-center items-center min-h-[50vh]'>
|
||||
<Loader2 className='h-8 w-8 animate-spin text-gray-500' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If not authenticated and not loading, this will show briefly before redirect
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className='flex p-5 items-center justify-center'>
|
||||
<h1>Unauthorized - Redirecting...</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='max-w-2xl min-w-sm mx-auto p-4'>
|
||||
<Card className='mb-8'>
|
||||
<CardHeader className='pb-2'>
|
||||
<CardTitle className='text-2xl'>Your Profile</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your personal information and how it appears to others
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
{isLoading && !profile ? (
|
||||
<div className='flex justify-center py-8'>
|
||||
<Loader2 className='h-8 w-8 animate-spin text-gray-500' />
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-8'>
|
||||
<AvatarUpload onAvatarUploaded={handleAvatarUploaded} />
|
||||
<Separator />
|
||||
<ProfileForm onSubmit={handleProfileSubmit} />
|
||||
<Separator />
|
||||
<ResetPasswordForm onSubmit={handleResetPasswordSubmit} />
|
||||
<Separator />
|
||||
<SignOut />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
<div className='w-full mx-auto text-center pt-2 md:pt-10'>
|
||||
<div className='mx-auto flex flex-col items-center justify-center'>
|
||||
<ProfileCard />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,172 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from '@/components/ui';
|
||||
import Link from 'next/link';
|
||||
import { signIn } from '@/lib/actions';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/components/context';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { StatusMessage, SubmitButton } from '@/components/default';
|
||||
import { Separator } from '@/components/ui';
|
||||
import { SignInWithApple, SignInWithMicrosoft } from '@/components/default/auth';
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: 'Please enter a valid email address.',
|
||||
}),
|
||||
password: z.string().min(8, {
|
||||
message: 'Password must be at least 8 characters.',
|
||||
}),
|
||||
});
|
||||
import { SignInCard } from '@/components/default/auth';
|
||||
|
||||
const Login = () => {
|
||||
const router = useRouter();
|
||||
const { isAuthenticated, isLoading, refreshUserData } = useAuth();
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
});
|
||||
|
||||
// Redirect if already authenticated
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
router.push('/');
|
||||
}
|
||||
}, [isAuthenticated, router]);
|
||||
|
||||
const handleSignIn = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
setStatusMessage('');
|
||||
const formData = new FormData();
|
||||
formData.append('email', values.email);
|
||||
formData.append('password', values.password);
|
||||
const result = await signIn(formData);
|
||||
if (result?.success) {
|
||||
await refreshUserData();
|
||||
form.reset();
|
||||
router.push('');
|
||||
} else {
|
||||
setStatusMessage(`Error: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatusMessage(
|
||||
`Error: ${error instanceof Error ? error.message : 'Could not sign in!'}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<div className='w-full mx-auto text-center pt-2 md:pt-10'>
|
||||
<div className='mx-auto flex flex-col items-center justify-center'>
|
||||
<Card className='min-w-xs md:min-w-sm sm:max-w-xs max-w-lg'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-3xl font-medium'>Sign In</CardTitle>
|
||||
<CardDescription className='text-foreground'>
|
||||
Don't have an account?{' '}
|
||||
<Link className='font-medium underline' href='/sign-up'>
|
||||
Sign up
|
||||
</Link>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSignIn)}
|
||||
className='flex flex-col min-w-64 space-y-6 pb-4'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-lg'>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='email'
|
||||
placeholder='you@example.com'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className='flex justify-between'>
|
||||
<FormLabel className='text-lg'>Password</FormLabel>
|
||||
<Link
|
||||
className='text-xs text-foreground underline text-right'
|
||||
href='/forgot-password'
|
||||
>
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='password'
|
||||
placeholder='Your password'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{statusMessage &&
|
||||
(statusMessage.includes('Error') ||
|
||||
statusMessage.includes('error') ||
|
||||
statusMessage.includes('failed') ||
|
||||
statusMessage.includes('invalid') ? (
|
||||
<StatusMessage message={{ error: statusMessage }} />
|
||||
) : (
|
||||
<StatusMessage message={{ message: statusMessage }} />
|
||||
))}
|
||||
<SubmitButton
|
||||
disabled={isLoading}
|
||||
pendingText='Signing In...'
|
||||
className='text-[1.0rem] cursor-pointer'
|
||||
>
|
||||
Sign in
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<div className='flex items-center w-full gap-4'>
|
||||
<Separator className='flex-1 bg-accent py-0.5' />
|
||||
<span className='text-sm text-muted-foreground'>or</span>
|
||||
<Separator className='flex-1 bg-accent py-0.5' />
|
||||
</div>
|
||||
<SignInWithMicrosoft />
|
||||
<SignInWithApple />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SignInCard />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,212 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import Link from 'next/link';
|
||||
import { signUp } from '@/lib/actions';
|
||||
import { StatusMessage, SubmitButton } from '@/components/default';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/components/context';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
Separator,
|
||||
} from '@/components/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
SignInWithApple,
|
||||
SignInWithMicrosoft,
|
||||
} from '@/components/default/auth';
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
name: z.string().min(2, {
|
||||
message: 'Name must be at least 2 characters.',
|
||||
}),
|
||||
email: z.string().email({
|
||||
message: 'Please enter a valid email address.',
|
||||
}),
|
||||
password: z.string().min(8, {
|
||||
message: 'Password must be at least 8 characters.',
|
||||
}),
|
||||
confirmPassword: z.string().min(8, {
|
||||
message: 'Password must be at least 8 characters.',
|
||||
}),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match!',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
import { SignUpCard } from "@/components/default/auth";
|
||||
|
||||
const SignUp = () => {
|
||||
const router = useRouter();
|
||||
const { isAuthenticated, isLoading, refreshUserData } = useAuth();
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
// Redirect if already authenticated
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
router.push('/');
|
||||
}
|
||||
}, [isAuthenticated, router]);
|
||||
|
||||
const handleSignUp = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
setStatusMessage('');
|
||||
const formData = new FormData();
|
||||
formData.append('name', values.name);
|
||||
formData.append('email', values.email);
|
||||
formData.append('password', values.password);
|
||||
const result = await signUp(formData);
|
||||
if (result?.success) {
|
||||
await refreshUserData();
|
||||
setStatusMessage(
|
||||
result.data ??
|
||||
'Thanks for signing up! Please check your email for a verification link.',
|
||||
);
|
||||
form.reset();
|
||||
router.push('');
|
||||
} else {
|
||||
setStatusMessage(`Error: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatusMessage(
|
||||
`Error: ${error instanceof Error ? error.message : 'Could not sign in!'}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full mx-auto text-center pt-2 md:pt-10'>
|
||||
<div className='mx-auto flex flex-col items-center justify-center'>
|
||||
<Card className='min-w-xs md:min-w-sm sm:max-w-xs lg:max-w-lg'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-3xl font-medium'>Sign Up</CardTitle>
|
||||
<CardDescription className='text-foreground'>
|
||||
Already have an account?{' '}
|
||||
<Link className='text-primary font-medium underline' href='/sign-in'>
|
||||
Sign in
|
||||
</Link>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSignUp)}
|
||||
className='flex flex-col mx-auto space-y-4 mb-4'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-lg'>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input type='text' placeholder='Full Name' {...field} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-lg'>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='email'
|
||||
placeholder='you@example.com'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-lg'>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='password'
|
||||
placeholder='Your password'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='confirmPassword'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-lg'>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='password'
|
||||
placeholder='Confirm password'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{statusMessage &&
|
||||
(statusMessage.includes('Error') ||
|
||||
statusMessage.includes('error') ||
|
||||
statusMessage.includes('failed') ||
|
||||
statusMessage.includes('invalid') ? (
|
||||
<StatusMessage message={{ error: statusMessage }} />
|
||||
) : (
|
||||
<StatusMessage message={{ success: statusMessage }} />
|
||||
))}
|
||||
<SubmitButton
|
||||
className='text-[1.0rem] cursor-pointer'
|
||||
disabled={isLoading}
|
||||
pendingText='Signing Up...'
|
||||
>
|
||||
Sign Up
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</Form>
|
||||
<div className='flex items-center w-full gap-4'>
|
||||
<Separator className='flex-1 bg-accent py-0.5' />
|
||||
<span className='text-sm text-muted-foreground'>or</span>
|
||||
<Separator className='flex-1 bg-accent py-0.5' />
|
||||
</div>
|
||||
<SignInWithMicrosoft type='signUp' />
|
||||
<SignInWithApple type='signUp' />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SignUpCard />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -10,6 +10,10 @@ const HomePage = async () => {
|
||||
redirect('/sign-in');
|
||||
}
|
||||
const user: User = response.data;
|
||||
return <NoSession />;
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello {user.email}</h1>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
export default HomePage;
|
||||
|
@ -1,11 +0,0 @@
|
||||
import { SignUpCard } from './cards/SignUp';
|
||||
|
||||
export default function NoSession() {
|
||||
return (
|
||||
<div className='w-full mx-auto text-center pt-2 md:pt-10'>
|
||||
<div className='mx-auto flex flex-col items-center justify-center'>
|
||||
<SignUpCard />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -92,30 +92,28 @@ export const ProfileCard = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='max-w-2xl min-w-sm mx-auto p-4'>
|
||||
<Card className='mb-8'>
|
||||
<CardHeader className='pb-2'>
|
||||
<CardTitle className='text-2xl'>Your Profile</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your personal information and how it appears to others
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
{isLoading && !profile ? (
|
||||
<div className='flex justify-center py-8'>
|
||||
<Loader2 className='h-8 w-8 animate-spin text-gray-500' />
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-8'>
|
||||
<AvatarUpload onAvatarUploaded={handleAvatarUploaded} />
|
||||
<Separator />
|
||||
<ProfileForm onSubmit={handleProfileSubmit} />
|
||||
<Separator />
|
||||
<ResetPasswordForm onSubmit={handleResetPasswordSubmit} />
|
||||
<Separator />
|
||||
<SignOut />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
<Card className='max-w-2xl min-w-xs sm:min-w-md mx-auto mb-8'>
|
||||
<CardHeader className='pb-2'>
|
||||
<CardTitle className='text-2xl'>Your Profile</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your personal information and how it appears to others
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
{isLoading && !profile ? (
|
||||
<div className='flex justify-center py-8'>
|
||||
<Loader2 className='h-8 w-8 animate-spin text-gray-500' />
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-8'>
|
||||
<AvatarUpload onAvatarUploaded={handleAvatarUploaded} />
|
||||
<Separator />
|
||||
<ProfileForm onSubmit={handleProfileSubmit} />
|
||||
<Separator />
|
||||
<ResetPasswordForm onSubmit={handleResetPasswordSubmit} />
|
||||
<Separator />
|
||||
<SignOut />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
@ -86,7 +86,7 @@ export const SignInCard = () => {
|
||||
<CardDescription className='text-foreground'>
|
||||
Don't have an account?{' '}
|
||||
<Link className='font-medium underline' href='/sign-up'>
|
||||
Sign up
|
||||
Sign Up
|
||||
</Link>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
@ -153,7 +153,7 @@ export const SignInCard = () => {
|
||||
pendingText='Signing In...'
|
||||
className='text-[1.0rem] cursor-pointer'
|
||||
>
|
||||
Sign in
|
||||
Sign In
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</Form>
|
||||
|
@ -105,7 +105,7 @@ export const SignUpCard = () => {
|
||||
<CardDescription className='text-foreground'>
|
||||
Already have an account?{' '}
|
||||
<Link className='text-primary font-medium underline' href='/sign-in'>
|
||||
Sign in
|
||||
Sign In
|
||||
</Link>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
@ -1,4 +1,3 @@
|
||||
export * from './NoSession';
|
||||
export * from './buttons/SignInSignUp';
|
||||
export * from './buttons/SignInWithApple';
|
||||
export * from './buttons/SignInWithMicrosoft';
|
||||
|
@ -15,24 +15,25 @@ const Footer = () => {
|
||||
flex items-center gap-2 transition-all duration-200'
|
||||
>
|
||||
<Image src='/icons/gitea.svg' alt='Gitea' width={20} height={20} />
|
||||
<span>View Source Code on Gitea</span>
|
||||
<span className='text-white'>View Source Code on Gitea</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className='flex-1 flex justify-center'>
|
||||
<div className='text-xs text-center space-y-1'>
|
||||
<p>
|
||||
<strong>Tech Tracker</strong> - Built for City of Gulfport IT Department
|
||||
</p>
|
||||
<div className='text-sm text-center space-y-1'>
|
||||
<div className='flex my-auto'>
|
||||
<Image
|
||||
src='/favicon.png'
|
||||
width={18}
|
||||
height={15}
|
||||
className='mr-1 mt-0.5'
|
||||
alt='Tech Tracker Logo'
|
||||
/>
|
||||
<p>
|
||||
<strong>Tech Tracker</strong> - City of Gulfport IT Department
|
||||
</p>
|
||||
</div>
|
||||
<p className='text-muted-foreground'>
|
||||
Open Source • MIT Licensed • Self-Hosted •
|
||||
<a
|
||||
href='https://supabase.com'
|
||||
target='_blank'
|
||||
className='hover:underline ml-1'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Powered by Supabase
|
||||
</a>
|
||||
Open Source • MIT Licensed • Self-Hosted
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,14 +12,15 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui';
|
||||
import { useAuth } from '@/components/context';
|
||||
import { useAuth, useTVMode } from '@/components/context';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { signOut } from '@/lib/actions';
|
||||
import { User } from 'lucide-react';
|
||||
|
||||
const AvatarDropdown = () => {
|
||||
const { profile, avatarUrl, isLoading, refreshUserData } = useAuth();
|
||||
const { profile, avatarUrl, refreshUserData } = useAuth();
|
||||
const router = useRouter();
|
||||
const { toggleTVMode, tvMode } = useTVMode();
|
||||
|
||||
const handleSignOut = async () => {
|
||||
const result = await signOut();
|
||||
@ -41,7 +42,7 @@ const AvatarDropdown = () => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Avatar className='cursor-pointer'>
|
||||
<Avatar className='cursor-pointer scale-125'>
|
||||
{avatarUrl ? (
|
||||
<AvatarImage
|
||||
src={avatarUrl}
|
||||
@ -50,19 +51,28 @@ const AvatarDropdown = () => {
|
||||
height={64}
|
||||
/>
|
||||
) : (
|
||||
<AvatarFallback className='text-sm'>
|
||||
<AvatarFallback className='text-md'>
|
||||
{profile?.full_name ? (
|
||||
getInitials(profile.full_name)
|
||||
) : (
|
||||
<User size={32} />
|
||||
<User size={64} />
|
||||
)}
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>{profile?.full_name}</DropdownMenuLabel>
|
||||
<DropdownMenuLabel className='font-bold'>{profile?.full_name}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<button
|
||||
onClick={toggleTVMode}
|
||||
className='w-full justify-center cursor-pointer'
|
||||
>
|
||||
{tvMode ? 'Normal Mode' : 'TV Mode'}
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className='h-[2px]' />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href='/profile'
|
@ -1,17 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import AvatarDropdown from './AvatarDropdown';
|
||||
import { SignInSignUp } from '@/components/default/auth';
|
||||
import { useAuth } from '@/components/context';
|
||||
|
||||
const NavigationAuth = () => {
|
||||
const { isAuthenticated } = useAuth();
|
||||
return isAuthenticated ? (
|
||||
<div className='flex items-center gap-4'>
|
||||
<AvatarDropdown />
|
||||
</div>
|
||||
) : (
|
||||
<SignInSignUp />
|
||||
);
|
||||
};
|
||||
export default NavigationAuth;
|
@ -2,23 +2,27 @@
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import NavigationAuth from './auth';
|
||||
import { ThemeToggle, TVToggle, useTVMode } from '@/components/context';
|
||||
import { ThemeToggle, useTVMode } from '@/components/context';
|
||||
import { useAuth } from '@/components/context';
|
||||
import AvatarDropdown from './AvatarDropdown';
|
||||
|
||||
const Header = () => {
|
||||
const { tvMode } = useTVMode();
|
||||
const { isAuthenticated } = useAuth();
|
||||
return tvMode ? (
|
||||
<div className='absolute top-4 right-2'>
|
||||
<div className='flex flex-row my-auto items-center pt-2 pr-0 md:pt-4'>
|
||||
<TVToggle />
|
||||
<div className='w-full py-2 pt-6 md:py-5'>
|
||||
<div className='absolute top-8 right-24'>
|
||||
<div className='flex flex-row my-auto items-center pt-2 pr-0 md:pt-4'>
|
||||
{isAuthenticated && <AvatarDropdown />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<header className='w-full py-2 pt-6 md:py-5'>
|
||||
<div className='absolute top-4 right-6'>
|
||||
<div className='absolute top-8 right-16'>
|
||||
<div className='flex flex-row my-auto items-center pt-2 pr-0 md:pt-4 md:pr-8'>
|
||||
<TVToggle />
|
||||
<ThemeToggle className='mx-2' />
|
||||
<ThemeToggle className='mr-4' />
|
||||
{isAuthenticated && <AvatarDropdown />}
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
@ -36,7 +40,7 @@ const Header = () => {
|
||||
/>
|
||||
<h1
|
||||
className='title-text text-sm md:text-4xl lg:text-8xl
|
||||
bg-gradient-to-r from-[#bec8e6] via-[#F0EEE4] to-[#FFF8E7]
|
||||
bg-gradient-to-r from-[#281A65] via-[#363354] to-accent-foreground dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]
|
||||
font-bold pl-2 md:pl-12 text-transparent bg-clip-text'
|
||||
>
|
||||
Tech Tracker
|
||||
|
@ -1,126 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Separator,
|
||||
} from '@/components/ui';
|
||||
import Link from 'next/link';
|
||||
import { CheckCircle, MessageCircleWarning } from 'lucide-react';
|
||||
|
||||
class SentryExampleFrontendError extends Error {
|
||||
constructor(message: string | undefined) {
|
||||
super(message);
|
||||
this.name = 'SentryExampleFrontendError';
|
||||
}
|
||||
}
|
||||
|
||||
export const TestSentryCard = () => {
|
||||
const [hasSentError, setHasSentError] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkConnectivity = async () => {
|
||||
console.log('Checking Sentry SDK connectivity...');
|
||||
const result = await Sentry.diagnoseSdkConnectivity();
|
||||
setIsConnected(result !== 'sentry-unreachable');
|
||||
};
|
||||
checkConnectivity().catch((error) => {
|
||||
console.error('Error trying to connect to Sentry: ', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const createError = async () => {
|
||||
await Sentry.startSpan(
|
||||
{
|
||||
name: 'Example Frontend Span',
|
||||
op: 'test',
|
||||
},
|
||||
async () => {
|
||||
const res = await fetch('/api/sentry/example');
|
||||
if (!res.ok) {
|
||||
setHasSentError(true);
|
||||
throw new SentryExampleFrontendError(
|
||||
'This error is raised in our TestSentry component on the main page.',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className='flex flex-row my-auto space-x-4'>
|
||||
<svg
|
||||
height='40'
|
||||
width='40'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M21.85 2.995a3.698 3.698 0 0 1 1.353 1.354l16.303 28.278a3.703 3.703 0 0 1-1.354 5.053 3.694 3.694 0 0 1-1.848.496h-3.828a31.149 31.149 0 0 0 0-3.09h3.815a.61.61 0 0 0 .537-.917L20.523 5.893a.61.61 0 0 0-1.057 0l-3.739 6.494a28.948 28.948 0 0 1 9.63 10.453 28.988 28.988 0 0 1 3.499 13.78v1.542h-9.852v-1.544a19.106 19.106 0 0 0-2.182-8.85 19.08 19.08 0 0 0-6.032-6.829l-1.85 3.208a15.377 15.377 0 0 1 6.382 12.484v1.542H3.696A3.694 3.694 0 0 1 0 34.473c0-.648.17-1.286.494-1.849l2.33-4.074a8.562 8.562 0 0 1 2.689 1.536L3.158 34.17a.611.611 0 0 0 .538.917h8.448a12.481 12.481 0 0 0-6.037-9.09l-1.344-.772 4.908-8.545 1.344.77a22.16 22.16 0 0 1 7.705 7.444 22.193 22.193 0 0 1 3.316 10.193h3.699a25.892 25.892 0 0 0-3.811-12.033 25.856 25.856 0 0 0-9.046-8.796l-1.344-.772 5.269-9.136a3.698 3.698 0 0 1 3.2-1.849c.648 0 1.285.17 1.847.495Z'
|
||||
fill='currentcolor'
|
||||
/>
|
||||
</svg>
|
||||
<CardTitle className='text-3xl my-auto'>Test Sentry</CardTitle>
|
||||
</div>
|
||||
<CardDescription className='text-[1.0rem]'>
|
||||
Click the button below & view the sample error on{' '}
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_SENTRY_URL}`}
|
||||
className='text-accent-foreground underline hover:text-primary'
|
||||
>
|
||||
the Sentry website
|
||||
</Link>
|
||||
. Navigate to the {"'"}Issues{"'"} page & you should see the sample
|
||||
error!
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='flex flex-row gap-4 my-auto'>
|
||||
<Button
|
||||
type='button'
|
||||
onClick={createError}
|
||||
className='cursor-pointer text-md my-auto py-6'
|
||||
>
|
||||
<span>Throw Sample Error</span>
|
||||
</Button>
|
||||
{hasSentError ? (
|
||||
<div className='rounded-md bg-green-500/80 dark:bg-green-500/50 py-2 px-4 flex flex-row gap-2 my-auto'>
|
||||
<CheckCircle size={30} className='my-auto' />
|
||||
<p className='text-lg'>Sample error was sent to Sentry!</p>
|
||||
</div>
|
||||
) : !isConnected ? (
|
||||
<div className='rounded-md bg-red-600/50 dark:bg-red-500/50 py-2 px-4 flex flex-row gap-2 my-auto'>
|
||||
<MessageCircleWarning size={40} className='my-auto' />
|
||||
<p>
|
||||
Wait! The Sentry SDK is not able to reach Sentry right now -
|
||||
this may be due to an adblocker. For more information, see{' '}
|
||||
<Link
|
||||
href='https://docs.sentry.io/platforms/javascript/guides/nextjs/troubleshooting/#the-sdk-is-not-sending-any-data'
|
||||
className='text-accent-foreground underline hover:text-primary'
|
||||
>
|
||||
the troubleshooting guide.
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className='success_placeholder' />
|
||||
)}
|
||||
</div>
|
||||
<Separator className='my-4 bg-accent' />
|
||||
<p className='description'>
|
||||
Warning! Sometimes Adblockers will prevent errors from being sent to
|
||||
Sentry.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export { TestSentryCard } from './TestSentry';
|
@ -76,11 +76,11 @@ SMTP_SENDER_NAME=fake_sender
|
||||
ENABLE_ANONYMOUS_USERS=false
|
||||
|
||||
|
||||
MAILER_TEMPLATES_INVITE="https://git.gbrown.org/gib/T3-Template/raw/branch/main/src/server/mail_templates/invite_user.html"
|
||||
MAILER_TEMPLATES_CONFIRMATION="https://git.gbrown.org/gib/T3-Template/raw/branch/main/src/server/mail_templates/confirm_signup.html"
|
||||
MAILER_TEMPLATES_RECOVERY="https://git.gbrown.org/gib/T3-Template/raw/branch/main/src/server/mail_templates/reset_password.html"
|
||||
MAILER_TEMPLATES_MAGIC_LINK="https://git.gbrown.org/gib/T3-Template/raw/branch/main/src/server/mail_templates/magic_link.html"
|
||||
MAILER_TEMPLATES_EMAIL_CHANGE="https://git.gbrown.org/gib/T3-Template/raw/branch/main/src/server/mail_templates/change_email_address.html"
|
||||
MAILER_TEMPLATES_INVITE="https://git.gbrown.org/gib/tech-tracker-next/raw/branch/main/src/server/mail_templates/invite_user.html"
|
||||
MAILER_TEMPLATES_CONFIRMATION="https://git.gbrown.org/gib/tech-tracker-next/raw/branch/main/src/server/mail_templates/confirm_signup.html"
|
||||
MAILER_TEMPLATES_RECOVERY="https://git.gbrown.org/gib/tech-tracker-next/raw/branch/main/src/server/mail_templates/reset_password.html"
|
||||
MAILER_TEMPLATES_MAGIC_LINK="https://git.gbrown.org/gib/tech-tracker-next/raw/branch/main/src/server/mail_templates/magic_link.html"
|
||||
MAILER_TEMPLATES_EMAIL_CHANGE="https://git.gbrown.org/gib/tech-tracker-next/raw/branch/main/src/server/mail_templates/change_email_address.html"
|
||||
|
||||
MAILER_SUBJECTS_INVITE="You've Been Invited!"
|
||||
MAILER_SUBJECTS_CONFIRMATION="Confirm Your Email"
|
||||
|
@ -9,7 +9,7 @@
|
||||
<table style="margin: 0 auto;">
|
||||
<tr>
|
||||
<td style="vertical-align: middle; padding-right: 15px;">
|
||||
<img src="https://git.gbrown.org/gib/Tech_Tracker_Web/raw/branch/master/public/images/tech_tracker_logo.png" alt="Tech Tracker Logo" width="80" height="auto" style="max-width: 80px; height: auto;">
|
||||
<img src="https://git.gbrown.org/gib/tech-tracker-next/raw/branch/main/public/favicon.png" alt="Tech Tracker Logo" width="80" height="auto" style="max-width: 80px; height: auto;">
|
||||
</td>
|
||||
<td style="vertical-align: middle;">
|
||||
<h1 style="margin: 0; font-size: 44px; color: #001084;">Tech Tracker</h1>
|
||||
|
Reference in New Issue
Block a user