Merge branch 'develop' into hackathon/fix-ui
This commit is contained in:
@@ -1,89 +0,0 @@
|
|||||||
import { FC, ReactElement, useEffect, useState } from 'react';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
import { useGitHubAuth } from '@imphnen-frontend-service/service';
|
|
||||||
import { useSession } from '@imphnen-frontend-service/utils';
|
|
||||||
import { GithubOutlined } from '@ant-design/icons';
|
|
||||||
|
|
||||||
const LoginPage: FC = (): ReactElement => {
|
|
||||||
console.log('[LoginPage] Rendering...');
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { signInWithGitHub } = useGitHubAuth();
|
|
||||||
const [isGithubLoading, setIsGithubLoading] = useState(false);
|
|
||||||
const { isAuthenticated } = useSession();
|
|
||||||
|
|
||||||
console.log('[LoginPage] isAuthenticated:', isAuthenticated);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('[LoginPage] useEffect - isAuthenticated:', isAuthenticated);
|
|
||||||
if (isAuthenticated) {
|
|
||||||
console.log('[LoginPage] Redirecting to dashboard...');
|
|
||||||
navigate('/dashboard');
|
|
||||||
}
|
|
||||||
}, [isAuthenticated, navigate]);
|
|
||||||
|
|
||||||
const handleGithubLogin = async () => {
|
|
||||||
try {
|
|
||||||
setIsGithubLoading(true);
|
|
||||||
console.log('[Login] Initiating GitHub OAuth...');
|
|
||||||
|
|
||||||
const result = await signInWithGitHub();
|
|
||||||
console.log('[Login] OAuth result:', result);
|
|
||||||
|
|
||||||
// Check if we got a redirect URL
|
|
||||||
if (result?.url) {
|
|
||||||
console.log('[Login] Redirecting to GitHub OAuth:', result.url);
|
|
||||||
// Supabase should handle the redirect automatically
|
|
||||||
// If we're still here after 2 seconds, manually redirect
|
|
||||||
setTimeout(() => {
|
|
||||||
if (result.url) {
|
|
||||||
globalThis.location.href = result.url;
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
} else {
|
|
||||||
console.error('[Login] No OAuth URL returned');
|
|
||||||
setIsGithubLoading(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Login] GitHub login failed:', error);
|
|
||||||
setIsGithubLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center items-center min-h-screen bg-gray-50 px-4">
|
|
||||||
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-100">
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">
|
|
||||||
Welcome to Hackathon
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
Sign in with your GitHub account to join or create your hackathon team
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className="w-full gap-2 py-3"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={handleGithubLogin}
|
|
||||||
disabled={isGithubLoading}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<GithubOutlined className="text-xl" />
|
|
||||||
<span className="font-semibold">
|
|
||||||
{isGithubLoading ? 'Connecting...' : 'Sign in with GitHub'}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<p className="text-gray-500 text-sm">
|
|
||||||
By signing in, you agree to our Terms of Service and Privacy Policy
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoginPage;
|
|
||||||
@@ -14,18 +14,14 @@ export default function RootLayout() {
|
|||||||
const checkAuth = async () => {
|
const checkAuth = async () => {
|
||||||
const pathname = location.pathname;
|
const pathname = location.pathname;
|
||||||
|
|
||||||
console.log('[Layout] Checking auth for route:', pathname);
|
|
||||||
|
|
||||||
// Allow hackathon pages without checks
|
// Allow hackathon pages without checks
|
||||||
if (pathname.startsWith('/hackathons')) {
|
if (pathname.startsWith('/hackathons')) {
|
||||||
console.log('[Layout] Public route, allowing access');
|
|
||||||
setIsChecking(false);
|
setIsChecking(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow auth callback without checks
|
// Allow auth callback without checks
|
||||||
if (pathname === '/auth/callback') {
|
if (pathname === '/auth/callback') {
|
||||||
console.log('[Layout] Auth callback, allowing access');
|
|
||||||
setIsChecking(false);
|
setIsChecking(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -43,27 +39,23 @@ export default function RootLayout() {
|
|||||||
if (isPublicAuthPage) {
|
if (isPublicAuthPage) {
|
||||||
// If already authenticated and not on password reset pages, redirect to dashboard
|
// If already authenticated and not on password reset pages, redirect to dashboard
|
||||||
if (session && pathname !== '/auth/reset-password') {
|
if (session && pathname !== '/auth/reset-password') {
|
||||||
console.log('[Layout] Already authenticated, redirecting to dashboard');
|
|
||||||
navigate('/dashboard', { replace: true });
|
navigate('/dashboard', { replace: true });
|
||||||
setIsChecking(false);
|
setIsChecking(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Allow unauthenticated access
|
// Allow unauthenticated access
|
||||||
console.log('[Layout] Public auth page, allowing access');
|
|
||||||
setIsChecking(false);
|
setIsChecking(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Home page - allow everyone to view the landing page
|
// Home page - allow everyone to view the landing page
|
||||||
if (pathname === '/') {
|
if (pathname === '/') {
|
||||||
console.log('[Layout] Landing page, allowing access');
|
|
||||||
setIsChecking(false);
|
setIsChecking(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Require authentication for all other routes
|
// Require authentication for all other routes
|
||||||
if (!session) {
|
if (!session) {
|
||||||
console.log('[Layout] No session, redirecting to login');
|
|
||||||
navigate('/auth/login', { replace: true });
|
navigate('/auth/login', { replace: true });
|
||||||
setIsChecking(false);
|
setIsChecking(false);
|
||||||
return;
|
return;
|
||||||
@@ -87,17 +79,15 @@ export default function RootLayout() {
|
|||||||
const hasLocation = !!userData?.location;
|
const hasLocation = !!userData?.location;
|
||||||
|
|
||||||
if (!hasLocation) {
|
if (!hasLocation) {
|
||||||
console.log('[Layout] User needs onboarding, redirecting');
|
|
||||||
navigate('/onboarding/user', { replace: true });
|
navigate('/onboarding/user', { replace: true });
|
||||||
setIsChecking(false);
|
setIsChecking(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Layout] Unexpected error checking onboarding:', error);
|
// Silently handle error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Layout] Auth check passed');
|
|
||||||
setIsChecking(false);
|
setIsChecking(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
useAuthStore,
|
useAuthStore,
|
||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { CitySelect } from '../../../components/city-select';
|
import { CitySelect } from '../../../components/city-select';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -70,41 +71,34 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
|||||||
|
|
||||||
const onSubmit = form.handleSubmit(async (data) => {
|
const onSubmit = form.handleSubmit(async (data) => {
|
||||||
try {
|
try {
|
||||||
// console.log('[Onboarding] Starting submission...', data);
|
|
||||||
let avatarUrl = session?.user?.avatar || null;
|
let avatarUrl = session?.user?.avatar || null;
|
||||||
|
|
||||||
// Upload avatar if a new file was selected
|
// Upload avatar if a new file was selected
|
||||||
if (avatarFile) {
|
if (avatarFile) {
|
||||||
// console.log('[Onboarding] Uploading avatar...');
|
|
||||||
const uploadResult = await uploadAvatar(avatarFile);
|
const uploadResult = await uploadAvatar(avatarFile);
|
||||||
avatarUrl = uploadResult.data.url;
|
avatarUrl = uploadResult.data.url;
|
||||||
// console.log('[Onboarding] Avatar uploaded:', avatarUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user in Supabase
|
// Update user in Supabase
|
||||||
// console.log('[Onboarding] Updating user in Supabase...');
|
await updateUser({
|
||||||
const result = await updateUser({
|
|
||||||
fullname: data.fullname,
|
fullname: data.fullname,
|
||||||
avatar: avatarUrl,
|
avatar: avatarUrl,
|
||||||
location: data.location,
|
location: data.location,
|
||||||
bio: data.bio,
|
bio: data.bio,
|
||||||
skills: data.skills,
|
skills: data.skills,
|
||||||
});
|
});
|
||||||
// console.log('[Onboarding] User updated successfully:', result);
|
|
||||||
|
|
||||||
// Wait a bit for the onSuccess handler to update localStorage
|
// Wait a bit for the onSuccess handler to update localStorage
|
||||||
// The updateUser mutation's onSuccess handler updates the Zustand store and localStorage
|
// The updateUser mutation's onSuccess handler updates the Zustand store and localStorage
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
// console.log('[Onboarding] Navigating to dashboard...');
|
|
||||||
// Use window.location for a full page reload to ensure middleware sees updated localStorage
|
// Use window.location for a full page reload to ensure middleware sees updated localStorage
|
||||||
globalThis.location.href = '/dashboard';
|
globalThis.location.href = '/dashboard';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error('[Onboarding] Onboarding failed:', error);
|
toast.error(
|
||||||
alert(
|
error instanceof Error
|
||||||
`Onboarding failed: ${
|
? error.message
|
||||||
error instanceof Error ? error.message : 'Unknown error'
|
: 'Onboarding failed. Please try again.'
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -138,8 +138,8 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
const isLoading = isUpdating || isUploading;
|
const isLoading = isUpdating || isUploading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 px-4 py-8">
|
<div className="fixed inset-0 bg-black/30 dark:bg-black/50 backdrop-blur-sm flex flex-col justify-center items-center px-4 py-8 z-50 overflow-y-auto">
|
||||||
<div className="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-xl shadow-lg">
|
<div className="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-xl shadow-xl dark:shadow-neutral-950/50">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { useNavigate, useParams } from 'react-router';
|
|||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { teamUpdateSchema, TTeamUpdateForm, useUpdateTeam, useTeamById, ETeamVisibility, useUploadFile, useAuthStore } from '@imphnen-frontend-service/service';
|
import { teamUpdateSchema, TTeamUpdateForm, useUpdateTeam, useTeamById, ETeamVisibility, useUploadFile, useAuthStore } from '@imphnen-frontend-service/service';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import INDONESIAN_CITIES from '../../../../constants/cities';
|
|
||||||
|
import { CitySelect } from '../../../../components/city-select';
|
||||||
|
|
||||||
const EditTeamPage: FC = (): ReactElement => {
|
const EditTeamPage: FC = (): ReactElement => {
|
||||||
const { teamId } = useParams<{ teamId: string }>();
|
const { teamId } = useParams<{ teamId: string }>();
|
||||||
@@ -229,22 +230,12 @@ const EditTeamPage: FC = (): ReactElement => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="city"
|
name="city"
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<div>
|
<CitySelect
|
||||||
<select
|
value={field.value || ''}
|
||||||
{...field}
|
onChange={field.onChange}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-neutral-600 dark:bg-neutral-800 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
error={fieldState.error?.message}
|
||||||
>
|
placeholder="Search your city..."
|
||||||
<option value="">Select city</option>
|
/>
|
||||||
{INDONESIAN_CITIES.map((city) => (
|
|
||||||
<option key={city} value={city}>
|
|
||||||
{city}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{fieldState.error && (
|
|
||||||
<p className="text-sm text-red-500 mt-1">{fieldState.error.message}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -85,54 +85,6 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
);
|
);
|
||||||
const canInvite = isLeader && members.length < MAX_TEAM_MEMBERS;
|
const canInvite = isLeader && members.length < MAX_TEAM_MEMBERS;
|
||||||
|
|
||||||
// Calculate total images to load
|
|
||||||
const totalImagesToLoad =
|
|
||||||
(team?.banner ? 1 : 0) +
|
|
||||||
(team?.logo ? 1 : 0) +
|
|
||||||
members.filter((m: any) => m.user?.avatar).length;
|
|
||||||
|
|
||||||
// Track image loading with timeout fallback
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoadingTeam && !isLoadingMembers && team) {
|
|
||||||
if (totalImagesToLoad === 0) {
|
|
||||||
setImagesLoaded(true);
|
|
||||||
} else if (imageLoadCount >= totalImagesToLoad) {
|
|
||||||
setImagesLoaded(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
isLoadingTeam,
|
|
||||||
isLoadingMembers,
|
|
||||||
team,
|
|
||||||
imageLoadCount,
|
|
||||||
totalImagesToLoad,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Fallback timeout - if images take too long, show content anyway
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoadingTeam && !isLoadingMembers && team && !imagesLoaded) {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
setImagesLoaded(true);
|
|
||||||
}, 3000); // 3 second timeout
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}, [isLoadingTeam, isLoadingMembers, team, imagesLoaded]);
|
|
||||||
|
|
||||||
const handleImageLoad = () => {
|
|
||||||
setImageLoadCount((prev) => prev + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Also count error as loaded to prevent stuck
|
|
||||||
const handleImageError = () => {
|
|
||||||
setImageLoadCount((prev) => prev + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Leader check:', {
|
|
||||||
currentUserId,
|
|
||||||
leaderId: team?.leader_id,
|
|
||||||
isLeader,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleInviteMember = async (e: React.FormEvent) => {
|
const handleInviteMember = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!inviteEmail.trim() || isInviting) return;
|
if (!inviteEmail.trim() || isInviting) return;
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
|||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const pathname = url.pathname;
|
const pathname = url.pathname;
|
||||||
|
|
||||||
console.log('[Middleware] Checking route:', pathname);
|
|
||||||
|
|
||||||
// Get session from Supabase (authoritative source)
|
// Get session from Supabase (authoritative source)
|
||||||
const { data: { session: supabaseSession }, error: sessionError } = await supabase.auth.getSession();
|
const { data: { session: supabaseSession }, error: sessionError } = await supabase.auth.getSession();
|
||||||
|
|
||||||
@@ -86,10 +84,8 @@ export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
|||||||
let hasLocation = false;
|
let hasLocation = false;
|
||||||
|
|
||||||
if (cached && (now - cached.timestamp) < CACHE_DURATION) {
|
if (cached && (now - cached.timestamp) < CACHE_DURATION) {
|
||||||
console.log('[Middleware] Using cached onboarding status');
|
|
||||||
hasLocation = cached.hasLocation;
|
hasLocation = cached.hasLocation;
|
||||||
} else {
|
} else {
|
||||||
console.log('[Middleware] Fetching fresh onboarding status');
|
|
||||||
const { data: userData, error: userError } = await supabase
|
const { data: userData, error: userError } = await supabase
|
||||||
.from('users')
|
.from('users')
|
||||||
.select('location')
|
.select('location')
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const Input: FC<TInputProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mergedClassName = cn(
|
const mergedClassName = cn(
|
||||||
`px-[12px] py-[8px] text-neutral-800 bg-white placeholder:text-neutral-300 border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 rounded-md font-bai-jamjuree w-full ${
|
`px-[12px] py-[8px] text-neutral-800 dark:text-white bg-white dark:bg-neutral-800 placeholder:text-neutral-300 dark:placeholder:text-neutral-500 border border-neutral-200 dark:border-neutral-600 hover:border-blue-300 dark:hover:border-blue-500 focus:outline-1 focus:outline-blue-500 dark:focus:outline-primary-500 rounded-md font-bai-jamjuree w-full ${
|
||||||
widthform === 'standard' ? 'min-w-70' : ''
|
widthform === 'standard' ? 'min-w-70' : ''
|
||||||
}`,
|
}`,
|
||||||
sizeClasses[size].textSize,
|
sizeClasses[size].textSize,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const Textarea: FC<TTextareaProps> = ({
|
|||||||
...rest
|
...rest
|
||||||
}): ReactElement => {
|
}): ReactElement => {
|
||||||
const mergedClassName = cn(
|
const mergedClassName = cn(
|
||||||
'rounded-md border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 px-[12px] py-[8px] invalid:border-danger-500 invalid:text-danger-500',
|
'rounded-md border border-neutral-200 dark:border-neutral-600 hover:border-blue-300 dark:hover:border-blue-500 focus:outline-1 focus:outline-blue-500 dark:focus:outline-primary-500 px-[12px] py-[8px] bg-white dark:bg-neutral-800 text-neutral-800 dark:text-white placeholder:text-neutral-300 dark:placeholder:text-neutral-500 invalid:border-danger-500 invalid:text-danger-500',
|
||||||
sizeClasses[size],
|
sizeClasses[size],
|
||||||
disabled && disabledClass,
|
disabled && disabledClass,
|
||||||
error && errorClass,
|
error && errorClass,
|
||||||
|
|||||||
Reference in New Issue
Block a user