feat: migrate all 5 Vite apps from react-router to TanStack Router
Migrated backoffice, hackathon, dimentorin, gacha, qrcampaign, and infra from custom react-router file-based routing to TanStack Router file-based routing following the tanstack-frontend-best-practice convention. Key changes per app: - New routes/ directory with __root.tsx, _public.tsx, _authenticated.tsx - Auth guards via beforeLoad (replaces old middleware.ts) - createFileRoute pattern for all page components - TanStackRouterVite plugin in vite.config for auto route generation - _components/_hooks folders colocated with routes (ignored by router) - routeTree.gen.ts auto-generated on dev/build Convention: - _public/* routes redirect to dashboard if authenticated - _authenticated/* routes redirect to /auth/login if not authenticated - $param for dynamic segments (was [param] in old convention) - _layout suffix for pathless layout routes Removed: - Old src/app/ directories from all apps - Old src/middleware.ts files - Custom convertPagesToRoute utility (no longer needed) - react-router dependency usage (kept in package.json for shared libs) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
86d9e759a6
commit
4240e8eb51
@@ -1,23 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<h1 className="text-9xl font-bold text-gray-200 mb-4">404</h1>
|
||||
<h2 className="text-3xl font-semibold text-gray-900 mb-4">
|
||||
Page Not Found
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-8">
|
||||
The page you are looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
Go Back Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { RequireAdmin } from '../features/admin/components/RequireAdmin';
|
||||
|
||||
export default function AdminLayout() {
|
||||
return (
|
||||
<RequireAdmin>
|
||||
<Outlet />
|
||||
</RequireAdmin>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function AdminPage() {
|
||||
return <div className="p-4">Select a menu item from the sidebar.</div>;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useRouteError, isRouteErrorResponse } from 'react-router-dom';
|
||||
|
||||
export default function ErrorPage() {
|
||||
const error = useRouteError();
|
||||
let errorMessage: string;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
errorMessage = error.statusText;
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
} else if (typeof error === 'string') {
|
||||
errorMessage = error;
|
||||
} else {
|
||||
console.error(error);
|
||||
errorMessage = 'Unknown error';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<h1 className="text-6xl font-bold text-red-600 mb-4">Oops!</h1>
|
||||
<p className="text-xl text-gray-700 mb-2">
|
||||
Sorry, an unexpected error has occurred.
|
||||
</p>
|
||||
<p className="text-gray-500 italic">{errorMessage}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../auth/store/auth.store';
|
||||
|
||||
interface RequireAdminProps {
|
||||
@@ -9,15 +7,14 @@ interface RequireAdminProps {
|
||||
export const RequireAdmin = ({ children }: RequireAdminProps) => {
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||
return null;
|
||||
}
|
||||
|
||||
const userRole = user?.role?.name;
|
||||
if (userRole !== 'Admin' && userRole !== 'Super Admin') {
|
||||
return <Navigate to="/" replace />;
|
||||
return null;
|
||||
}
|
||||
|
||||
return children;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Outlet, Link, useLocation } from 'react-router-dom';
|
||||
import { Outlet, Link, useLocation } from '@tanstack/react-router';
|
||||
import { useAuthStore } from '../../auth/store/auth.store';
|
||||
|
||||
export const AdminDashboard = () => {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import React from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../../features/auth/store/auth.store';
|
||||
import { useAuthStore } from '../store/auth.store';
|
||||
|
||||
interface RequireAuthProps {
|
||||
children: JSX.Element;
|
||||
@@ -8,10 +6,9 @@ interface RequireAuthProps {
|
||||
|
||||
export const RequireAuth = ({ children }: RequireAuthProps) => {
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||
return null;
|
||||
}
|
||||
|
||||
return children;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAuthStore } from '../store/auth.store';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useNavigate, useLocation } from '@tanstack/react-router';
|
||||
|
||||
interface LoginFormInputs {
|
||||
email: string;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { FC, ReactElement, useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Link, useLocation } from '@tanstack/react-router';
|
||||
import { cn, For } from '@imphnen-frontend-service/utils';
|
||||
import { useAuthStore } from '../app/features/auth/store/auth.store';
|
||||
|
||||
|
||||
@@ -1,42 +1,28 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { StrictMode } from 'react';
|
||||
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router';
|
||||
import {
|
||||
add404PageToRoutesChildren,
|
||||
addErrorElementToRoutes,
|
||||
convertPagesToRoute,
|
||||
ModalLoginProvider,
|
||||
QueryProvider,
|
||||
} from '@imphnen-frontend-service/utils';
|
||||
import { Toaster } from 'sonner';
|
||||
import './index.css';
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
||||
import { QueryProvider } from '@imphnen-frontend-service/utils'
|
||||
import { Toaster } from 'sonner'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
import './index.css'
|
||||
|
||||
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
||||
const errorFiles = import.meta.glob('./app/**/*error.tsx');
|
||||
const notFoundFiles = import.meta.glob('./app/**/*404.tsx');
|
||||
const loadingFiles = import.meta.glob('./app/**/*loading.tsx');
|
||||
const router = createRouter({ routeTree })
|
||||
|
||||
const routes = convertPagesToRoute(files, loadingFiles) as RouteObject;
|
||||
addErrorElementToRoutes(errorFiles, routes);
|
||||
add404PageToRoutesChildren(notFoundFiles, routes);
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router
|
||||
}
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
...routes,
|
||||
},
|
||||
]);
|
||||
const rootElement = document.getElementById('root')
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
if (!rootElement) throw new Error('Failed to find the root element');
|
||||
if (!rootElement) throw new Error('Failed to find the root element')
|
||||
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<QueryProvider>
|
||||
<ModalLoginProvider>
|
||||
<Toaster position="top-right" richColors />
|
||||
<RouterProvider router={router} />
|
||||
</ModalLoginProvider>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster position="top-right" richColors />
|
||||
</QueryProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
// This file is auto-generated by TanStack Router
|
||||
export const routeTree = {} as any
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createRootRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: () => <Outlet />,
|
||||
})
|
||||
+15
-43
@@ -1,47 +1,21 @@
|
||||
import {
|
||||
Outlet,
|
||||
ScrollRestoration,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAuthStore } from './features/auth/store/auth.store';
|
||||
import { Sidebar } from '../components/Sidebar';
|
||||
import { MenuOutlined } from '@ant-design/icons';
|
||||
|
||||
const isPublicRoute = (pathname: string) => {
|
||||
return pathname.startsWith('/auth') || pathname === '/auth/callback';
|
||||
};
|
||||
|
||||
export default function RootLayout() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPublicRoute(location.pathname)) {
|
||||
return;
|
||||
}
|
||||
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
|
||||
import { useAuthStore } from '../app/features/auth/store/auth.store'
|
||||
import { Sidebar } from '../components/Sidebar'
|
||||
import { MenuOutlined } from '@ant-design/icons'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated')({
|
||||
beforeLoad: () => {
|
||||
const { isAuthenticated } = useAuthStore.getState()
|
||||
if (!isAuthenticated) {
|
||||
navigate('/auth/login', { replace: true });
|
||||
return;
|
||||
throw redirect({ to: '/auth/login' })
|
||||
}
|
||||
}, [location.pathname, navigate, isAuthenticated]);
|
||||
},
|
||||
component: AuthenticatedLayout,
|
||||
})
|
||||
|
||||
if (isPublicRoute(location.pathname)) {
|
||||
return (
|
||||
<>
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
function AuthenticatedLayout() {
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gray-50">
|
||||
@@ -67,8 +41,6 @@ export default function RootLayout() {
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<ScrollRestoration />
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
|
||||
import { useAuthStore } from '../../app/features/auth/store/auth.store'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/admin')({
|
||||
beforeLoad: () => {
|
||||
const { user, isAuthenticated } = useAuthStore.getState()
|
||||
if (!isAuthenticated) {
|
||||
throw redirect({ to: '/auth/login' })
|
||||
}
|
||||
const userRole = user?.role?.name
|
||||
if (userRole !== 'Admin' && userRole !== 'Super Admin') {
|
||||
throw redirect({ to: '/' })
|
||||
}
|
||||
},
|
||||
component: () => <Outlet />,
|
||||
})
|
||||
+54
-49
@@ -1,88 +1,93 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms'
|
||||
import {
|
||||
campaignService,
|
||||
Campaign,
|
||||
CreateCampaignRequest,
|
||||
} from '../../features/admin/api/campaign.service';
|
||||
} from '../../../app/features/admin/api/campaign.service'
|
||||
import {
|
||||
DeleteOutlined,
|
||||
CheckCircleOutlined,
|
||||
PlusOutlined,
|
||||
FileOutlined,
|
||||
} from '@ant-design/icons';
|
||||
} from '@ant-design/icons'
|
||||
|
||||
export default function CampaignsPage() {
|
||||
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
export const Route = createFileRoute('/_authenticated/admin/campaigns')({
|
||||
component: CampaignsPage,
|
||||
})
|
||||
|
||||
function CampaignsPage() {
|
||||
const [campaigns, setCampaigns] = useState<Campaign[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [createLoading, setCreateLoading] = useState(false)
|
||||
const [formData, setFormData] = useState<CreateCampaignRequest>({
|
||||
name: '',
|
||||
url: '',
|
||||
});
|
||||
})
|
||||
|
||||
const fetchCampaigns = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await campaignService.getCampaigns();
|
||||
setCampaigns(data);
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const data = await campaignService.getCampaigns()
|
||||
setCampaigns(data)
|
||||
} catch (err) {
|
||||
setError('Failed to load campaigns');
|
||||
console.error('Error fetching campaigns:', err);
|
||||
setError('Failed to load campaigns')
|
||||
console.error('Error fetching campaigns:', err)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchCampaigns();
|
||||
}, []);
|
||||
fetchCampaigns()
|
||||
}, [])
|
||||
|
||||
const handleCreateCampaign = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
try {
|
||||
setCreateLoading(true);
|
||||
await campaignService.createCampaign(formData);
|
||||
setShowCreateModal(false);
|
||||
setFormData({ name: '', url: '' });
|
||||
await fetchCampaigns();
|
||||
setCreateLoading(true)
|
||||
await campaignService.createCampaign(formData)
|
||||
setShowCreateModal(false)
|
||||
setFormData({ name: '', url: '' })
|
||||
await fetchCampaigns()
|
||||
} catch (err) {
|
||||
console.error('Error creating campaign:', err);
|
||||
alert('Failed to create campaign');
|
||||
console.error('Error creating campaign:', err)
|
||||
alert('Failed to create campaign')
|
||||
} finally {
|
||||
setCreateLoading(false);
|
||||
setCreateLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleActivateCampaign = async (campaignId: string) => {
|
||||
try {
|
||||
await campaignService.activateCampaign(campaignId);
|
||||
await fetchCampaigns();
|
||||
await campaignService.activateCampaign(campaignId)
|
||||
await fetchCampaigns()
|
||||
} catch (err) {
|
||||
console.error('Error activating campaign:', err);
|
||||
alert('Failed to activate campaign');
|
||||
console.error('Error activating campaign:', err)
|
||||
alert('Failed to activate campaign')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDeleteCampaign = async (
|
||||
campaignId: string,
|
||||
campaignName: string
|
||||
) => {
|
||||
if (!confirm(`Are you sure you want to delete "${campaignName}"?`)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
try {
|
||||
await campaignService.deleteCampaign(campaignId);
|
||||
await fetchCampaigns();
|
||||
await campaignService.deleteCampaign(campaignId)
|
||||
await fetchCampaigns()
|
||||
} catch (err) {
|
||||
console.error('Error deleting campaign:', err);
|
||||
alert('Failed to delete campaign');
|
||||
console.error('Error deleting campaign:', err)
|
||||
alert('Failed to delete campaign')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const columns: ColumnDef<Campaign>[] = [
|
||||
{
|
||||
@@ -156,7 +161,7 @@ export default function CampaignsPage() {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -165,7 +170,7 @@ export default function CampaignsPage() {
|
||||
<div className="text-gray-600">Loading campaigns...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
@@ -175,7 +180,7 @@ export default function CampaignsPage() {
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -260,8 +265,8 @@ export default function CampaignsPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowCreateModal(false);
|
||||
setFormData({ name: '', url: '' });
|
||||
setShowCreateModal(false)
|
||||
setFormData({ name: '', url: '' })
|
||||
}}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors cursor-pointer"
|
||||
disabled={createLoading}
|
||||
@@ -281,5 +286,5 @@ export default function CampaignsPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/admin/')({
|
||||
component: AdminPage,
|
||||
})
|
||||
|
||||
function AdminPage() {
|
||||
return <div className="p-4">Select a menu item from the sidebar.</div>
|
||||
}
|
||||
+51
-46
@@ -1,63 +1,68 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { userService, User } from '../../features/admin/api/user.service';
|
||||
import { DeleteOutlined, EditOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms'
|
||||
import { userService, User } from '../../../app/features/admin/api/user.service'
|
||||
import { DeleteOutlined, EditOutlined, UserOutlined } from '@ant-design/icons'
|
||||
|
||||
export default function UsersPage() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [editingUserId, setEditingUserId] = useState<string | null>(null);
|
||||
const [selectedRole, setSelectedRole] = useState<string>('');
|
||||
export const Route = createFileRoute('/_authenticated/admin/users')({
|
||||
component: UsersPage,
|
||||
})
|
||||
|
||||
function UsersPage() {
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [editingUserId, setEditingUserId] = useState<string | null>(null)
|
||||
const [selectedRole, setSelectedRole] = useState<string>('')
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await userService.getUsers();
|
||||
setUsers(data);
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const data = await userService.getUsers()
|
||||
setUsers(data)
|
||||
} catch (err) {
|
||||
setError('Failed to load users');
|
||||
console.error('Error fetching users:', err);
|
||||
setError('Failed to load users')
|
||||
console.error('Error fetching users:', err)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
fetchUsers()
|
||||
}, [])
|
||||
|
||||
const handleUpdateRole = async (userId: string, currentRole: string) => {
|
||||
if (editingUserId === userId) {
|
||||
try {
|
||||
await userService.updateUserRole(userId, selectedRole);
|
||||
setEditingUserId(null);
|
||||
setSelectedRole('');
|
||||
await fetchUsers();
|
||||
await userService.updateUserRole(userId, selectedRole)
|
||||
setEditingUserId(null)
|
||||
setSelectedRole('')
|
||||
await fetchUsers()
|
||||
} catch (err) {
|
||||
console.error('Error updating user role:', err);
|
||||
alert('Failed to update user role');
|
||||
console.error('Error updating user role:', err)
|
||||
alert('Failed to update user role')
|
||||
}
|
||||
} else {
|
||||
setEditingUserId(userId);
|
||||
setSelectedRole(currentRole);
|
||||
setEditingUserId(userId)
|
||||
setSelectedRole(currentRole)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDeleteUser = async (userId: string, userName: string) => {
|
||||
if (!confirm(`Are you sure you want to delete user "${userName}"?`)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
try {
|
||||
await userService.deleteUser(userId);
|
||||
await fetchUsers();
|
||||
await userService.deleteUser(userId)
|
||||
await fetchUsers()
|
||||
} catch (err) {
|
||||
console.error('Error deleting user:', err);
|
||||
alert('Failed to delete user');
|
||||
console.error('Error deleting user:', err)
|
||||
alert('Failed to delete user')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const columns: ColumnDef<User>[] = [
|
||||
{
|
||||
@@ -78,7 +83,7 @@ export default function UsersPage() {
|
||||
accessorKey: 'role',
|
||||
header: 'Role',
|
||||
cell: ({ row }) => {
|
||||
const isEditing = editingUserId === row.original.id;
|
||||
const isEditing = editingUserId === row.original.id
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{isEditing ? (
|
||||
@@ -103,7 +108,7 @@ export default function UsersPage() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -119,7 +124,7 @@ export default function UsersPage() {
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => {
|
||||
const isEditing = editingUserId === row.original.id;
|
||||
const isEditing = editingUserId === row.original.id
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
@@ -138,8 +143,8 @@ export default function UsersPage() {
|
||||
{isEditing && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingUserId(null);
|
||||
setSelectedRole('');
|
||||
setEditingUserId(null)
|
||||
setSelectedRole('')
|
||||
}}
|
||||
className="p-2 text-gray-600 hover:bg-gray-50 rounded-md transition-colors cursor-pointer"
|
||||
title="Cancel"
|
||||
@@ -159,10 +164,10 @@ export default function UsersPage() {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -171,7 +176,7 @@ export default function UsersPage() {
|
||||
<div className="text-gray-600">Loading users...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
@@ -181,7 +186,7 @@ export default function UsersPage() {
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -209,5 +214,5 @@ export default function UsersPage() {
|
||||
<DataTable data={users} columns={columns} pageSize={10} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
+43
-38
@@ -1,31 +1,36 @@
|
||||
import { useState } from 'react';
|
||||
import { Dropzone } from './features/watermark/components/Dropzone';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from './features/auth/api/auth.service';
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { Dropzone } from '../../app/features/watermark/components/Dropzone'
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { toast } from 'sonner'
|
||||
import { api } from '../../app/features/auth/api/auth.service'
|
||||
|
||||
export default function HomePage() {
|
||||
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||
const [generatedImage, setGeneratedImage] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
export const Route = createFileRoute('/_authenticated/')({
|
||||
component: HomePage,
|
||||
})
|
||||
|
||||
function HomePage() {
|
||||
const [imageFile, setImageFile] = useState<File | null>(null)
|
||||
const [generatedImage, setGeneratedImage] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleImageDropped = (file: File) => {
|
||||
setImageFile(file);
|
||||
setGeneratedImage(null);
|
||||
toast.success('Image selected ready for generation!');
|
||||
};
|
||||
setImageFile(file)
|
||||
setGeneratedImage(null)
|
||||
toast.success('Image selected ready for generation!')
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setImageFile(null);
|
||||
setGeneratedImage(null);
|
||||
};
|
||||
setImageFile(null)
|
||||
setGeneratedImage(null)
|
||||
}
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!imageFile) return;
|
||||
if (!imageFile) return
|
||||
|
||||
setIsLoading(true);
|
||||
const formData = new FormData();
|
||||
formData.append('image', imageFile);
|
||||
setIsLoading(true)
|
||||
const formData = new FormData()
|
||||
formData.append('image', imageFile)
|
||||
|
||||
try {
|
||||
const response = await api.post('/campaigns/process-image', formData, {
|
||||
@@ -33,30 +38,30 @@ export default function HomePage() {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
responseType: 'blob',
|
||||
});
|
||||
})
|
||||
|
||||
const imageUrl = URL.createObjectURL(response.data);
|
||||
setGeneratedImage(imageUrl);
|
||||
toast.success('QR Code generated successfully!');
|
||||
const imageUrl = URL.createObjectURL(response.data)
|
||||
setGeneratedImage(imageUrl)
|
||||
toast.success('QR Code generated successfully!')
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
console.error(error)
|
||||
const message =
|
||||
error.response?.data?.message || 'Failed to generate QR code.';
|
||||
toast.error(message);
|
||||
error.response?.data?.message || 'Failed to generate QR code.'
|
||||
toast.error(message)
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!generatedImage) return;
|
||||
const link = document.createElement('a');
|
||||
link.href = generatedImage;
|
||||
link.download = `qr-campaign-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
if (!generatedImage) return
|
||||
const link = document.createElement('a')
|
||||
link.href = generatedImage
|
||||
link.download = `qr-campaign-${Date.now()}.png`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-8 px-4 flex flex-col gap-8">
|
||||
@@ -140,5 +145,5 @@ export default function HomePage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
|
||||
import { useAuthStore } from '../app/features/auth/store/auth.store'
|
||||
|
||||
export const Route = createFileRoute('/_public')({
|
||||
beforeLoad: () => {
|
||||
const { isAuthenticated } = useAuthStore.getState()
|
||||
if (isAuthenticated) {
|
||||
throw redirect({ to: '/' })
|
||||
}
|
||||
},
|
||||
component: PublicLayout,
|
||||
})
|
||||
|
||||
function PublicLayout() {
|
||||
return <Outlet />
|
||||
}
|
||||
+25
-21
@@ -1,32 +1,36 @@
|
||||
import { useState } from 'react';
|
||||
import { useForgotPassword } from '@imphnen-frontend-service/service';
|
||||
import { Link, useNavigate } from 'react-router';
|
||||
import { toast } from 'sonner';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { useForgotPassword } from '@imphnen-frontend-service/service'
|
||||
import { toast } from 'sonner'
|
||||
import { Icon } from '@iconify/react'
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [emailSent, setEmailSent] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const forgotPasswordMutation = useForgotPassword();
|
||||
export const Route = createFileRoute('/_public/auth/forgot-password')({
|
||||
component: ForgotPasswordPage,
|
||||
})
|
||||
|
||||
function ForgotPasswordPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [emailSent, setEmailSent] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const forgotPasswordMutation = useForgotPassword()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
if (!email) {
|
||||
toast.error('Please enter your email');
|
||||
return;
|
||||
toast.error('Please enter your email')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await forgotPasswordMutation.mutateAsync({ email });
|
||||
await forgotPasswordMutation.mutateAsync({ email })
|
||||
|
||||
setEmailSent(true);
|
||||
toast.success('Password reset email sent! Check your inbox.');
|
||||
setEmailSent(true)
|
||||
toast.success('Password reset email sent! Check your inbox.')
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message || 'Failed to send reset email');
|
||||
toast.error((err as Error).message || 'Failed to send reset email')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (emailSent) {
|
||||
return (
|
||||
@@ -65,7 +69,7 @@ export default function ForgotPasswordPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -73,7 +77,7 @@ export default function ForgotPasswordPage() {
|
||||
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<button
|
||||
onClick={() => navigate('/auth/login')}
|
||||
onClick={() => navigate({ to: '/auth/login' })}
|
||||
className="cursor-pointer text-primary-500 hover:text-primary-600 text-base font-sans flex items-center"
|
||||
>
|
||||
<Icon icon="ic:baseline-chevron-left" width="24" height="24" />
|
||||
@@ -121,5 +125,5 @@ export default function ForgotPasswordPage() {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
+34
-30
@@ -1,46 +1,50 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { GithubOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, Link } from 'react-router';
|
||||
import { toast } from 'sonner';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useAuthStore } from '../../features/auth/store/auth.store';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { GithubOutlined } from '@ant-design/icons'
|
||||
import { toast } from 'sonner'
|
||||
import { Icon } from '@iconify/react'
|
||||
import { useAuthStore } from '../../../app/features/auth/store/auth.store'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().min(1, 'Email is required').email('Please enter a valid email'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
});
|
||||
type LoginForm = z.infer<typeof loginSchema>;
|
||||
})
|
||||
type LoginForm = z.infer<typeof loginSchema>
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const login = useAuthStore((state) => state.login);
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const [isGithubLoading, setIsGithubLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
export const Route = createFileRoute('/_public/auth/login')({
|
||||
component: LoginPage,
|
||||
})
|
||||
|
||||
function LoginPage() {
|
||||
const navigate = useNavigate()
|
||||
const login = useAuthStore((state) => state.login)
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)
|
||||
const [isGithubLoading, setIsGithubLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const { register, handleSubmit, formState: { errors, isValid } } = useForm<LoginForm>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
mode: 'onChange',
|
||||
defaultValues: { email: '', password: '' },
|
||||
});
|
||||
})
|
||||
|
||||
useEffect(() => { if (isAuthenticated) navigate('/'); }, [isAuthenticated, navigate]);
|
||||
useEffect(() => { if (isAuthenticated) navigate({ to: '/' }) }, [isAuthenticated, navigate])
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
setError(null);
|
||||
setIsSubmitting(true);
|
||||
setError(null)
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
const success = await login(data.email, data.password);
|
||||
if (success) { toast.success('Login successful!'); navigate('/'); }
|
||||
else setError('Login failed. Please check your credentials.');
|
||||
} catch { setError('Login failed. Please try again.'); }
|
||||
finally { setIsSubmitting(false); }
|
||||
});
|
||||
const success = await login(data.email, data.password)
|
||||
if (success) { toast.success('Login successful!'); navigate({ to: '/' }) }
|
||||
else setError('Login failed. Please check your credentials.')
|
||||
} catch { setError('Login failed. Please try again.') }
|
||||
finally { setIsSubmitting(false) }
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
|
||||
@@ -105,5 +109,5 @@ export default function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
+40
-36
@@ -1,70 +1,74 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
useResetPassword,
|
||||
useAuthStore,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { toast } from 'sonner';
|
||||
import { Icon } from '@iconify/react';
|
||||
} from '@imphnen-frontend-service/service'
|
||||
import { toast } from 'sonner'
|
||||
import { Icon } from '@iconify/react'
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
const navigate = useNavigate();
|
||||
const { clearSession } = useAuthStore();
|
||||
const resetPasswordMutation = useResetPassword();
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
export const Route = createFileRoute('/_public/auth/reset-password')({
|
||||
component: ResetPasswordPage,
|
||||
})
|
||||
|
||||
function ResetPasswordPage() {
|
||||
const navigate = useNavigate()
|
||||
const { clearSession } = useAuthStore()
|
||||
const resetPasswordMutation = useResetPassword()
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const hashParams = new URLSearchParams(
|
||||
globalThis.location.hash.substring(1)
|
||||
);
|
||||
const queryParams = new URLSearchParams(globalThis.location.search);
|
||||
)
|
||||
const queryParams = new URLSearchParams(globalThis.location.search)
|
||||
const token =
|
||||
hashParams.get('access_token') || queryParams.get('access_token');
|
||||
hashParams.get('access_token') || queryParams.get('access_token')
|
||||
|
||||
if (token) {
|
||||
setAccessToken(token);
|
||||
setAccessToken(token)
|
||||
} else {
|
||||
toast.error('Invalid or expired reset link');
|
||||
setTimeout(() => navigate('/auth/forgot-password'), 2000);
|
||||
toast.error('Invalid or expired reset link')
|
||||
setTimeout(() => navigate({ to: '/auth/forgot-password' }), 2000)
|
||||
}
|
||||
}, [navigate]);
|
||||
}, [navigate])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
toast.error('Passwords do not match');
|
||||
return;
|
||||
toast.error('Passwords do not match')
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
toast.error('Password must be at least 6 characters');
|
||||
return;
|
||||
toast.error('Password must be at least 6 characters')
|
||||
return
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
toast.error('Invalid reset token');
|
||||
return;
|
||||
toast.error('Invalid reset token')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await resetPasswordMutation.mutateAsync({
|
||||
access_token: accessToken,
|
||||
new_password: password,
|
||||
});
|
||||
})
|
||||
|
||||
toast.success('Password updated successfully!');
|
||||
toast.success('Password updated successfully!')
|
||||
|
||||
clearSession();
|
||||
navigate('/auth/login');
|
||||
clearSession()
|
||||
navigate({ to: '/auth/login' })
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message || 'Failed to reset password');
|
||||
toast.error((err as Error).message || 'Failed to reset password')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
return (
|
||||
@@ -74,7 +78,7 @@ export default function ResetPasswordPage() {
|
||||
<p className="text-gray-600">Verifying reset link...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -164,5 +168,5 @@ export default function ResetPasswordPage() {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
+50
-46
@@ -1,13 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { useGitHubAuth } from '@imphnen-frontend-service/service';
|
||||
import { GithubOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, Link } from 'react-router';
|
||||
import { toast } from 'sonner';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useAuthStore } from '../../features/auth/store/auth.store';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { useGitHubAuth } from '@imphnen-frontend-service/service'
|
||||
import { GithubOutlined } from '@ant-design/icons'
|
||||
import { toast } from 'sonner'
|
||||
import { Icon } from '@iconify/react'
|
||||
import { useAuthStore } from '../../../app/features/auth/store/auth.store'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
|
||||
const signupSchema = z
|
||||
.object({
|
||||
@@ -28,20 +28,24 @@ const signupSchema = z
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
})
|
||||
|
||||
type SignupFormData = z.infer<typeof signupSchema>;
|
||||
type SignupFormData = z.infer<typeof signupSchema>
|
||||
|
||||
export default function SignupPage() {
|
||||
const navigate = useNavigate();
|
||||
const registerUser = useAuthStore((state) => state.register);
|
||||
export const Route = createFileRoute('/_public/auth/signup')({
|
||||
component: SignupPage,
|
||||
})
|
||||
|
||||
const { signInWithGitHub } = useGitHubAuth();
|
||||
const [isGithubLoading, setIsGithubLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
function SignupPage() {
|
||||
const navigate = useNavigate()
|
||||
const registerUser = useAuthStore((state) => state.register)
|
||||
|
||||
const { signInWithGitHub } = useGitHubAuth()
|
||||
const [isGithubLoading, setIsGithubLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -50,56 +54,56 @@ export default function SignupPage() {
|
||||
} = useForm<SignupFormData>({
|
||||
resolver: zodResolver(signupSchema),
|
||||
mode: 'onChange',
|
||||
});
|
||||
})
|
||||
|
||||
const onSubmit = async (data: SignupFormData) => {
|
||||
setError(null);
|
||||
setIsSubmitting(true);
|
||||
setError(null)
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
await registerUser(data.fullname, data.email, data.password);
|
||||
toast.success('Registration successful! Redirecting...');
|
||||
navigate('/');
|
||||
await registerUser(data.fullname, data.email, data.password)
|
||||
toast.success('Registration successful! Redirecting...')
|
||||
navigate({ to: '/' })
|
||||
} catch (err: any) {
|
||||
console.error('[Signup] Email signup failed:', err);
|
||||
console.error('[Signup] Email signup failed:', err)
|
||||
const errorMessage =
|
||||
err.response?.data?.message || err.message || 'Signup failed';
|
||||
setError(errorMessage);
|
||||
err.response?.data?.message || err.message || 'Signup failed'
|
||||
setError(errorMessage)
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleGithubLogin = async () => {
|
||||
try {
|
||||
setIsGithubLoading(true);
|
||||
setIsGithubLoading(true)
|
||||
|
||||
const result = await signInWithGitHub();
|
||||
const result = await signInWithGitHub()
|
||||
|
||||
if (result?.url) {
|
||||
globalThis.location.href = result.url;
|
||||
globalThis.location.href = result.url
|
||||
} else {
|
||||
setIsGithubLoading(false);
|
||||
setError('Failed to get GitHub OAuth URL');
|
||||
setIsGithubLoading(false)
|
||||
setError('Failed to get GitHub OAuth URL')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Signup] GitHub login failed:', err);
|
||||
setError((err as Error).message || 'GitHub login failed');
|
||||
setIsGithubLoading(false);
|
||||
console.error('[Signup] GitHub login failed:', err)
|
||||
setError((err as Error).message || 'GitHub login failed')
|
||||
setIsGithubLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const inputBaseClass =
|
||||
'w-full px-4 py-2.5 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white text-gray-900 placeholder:text-gray-400 disabled:bg-gray-100 disabled:cursor-not-allowed';
|
||||
const inputErrorClass = 'border-red-500';
|
||||
const inputNormalClass = 'border-gray-300';
|
||||
'w-full px-4 py-2.5 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white text-gray-900 placeholder:text-gray-400 disabled:bg-gray-100 disabled:cursor-not-allowed'
|
||||
const inputErrorClass = 'border-red-500'
|
||||
const inputNormalClass = 'border-gray-300'
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
|
||||
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
onClick={() => navigate({ to: '/' })}
|
||||
className="cursor-pointer text-primary-500 hover:text-primary-600 text-base font-sans flex items-center"
|
||||
>
|
||||
<Icon icon="ic:baseline-chevron-left" width="24" height="24" />
|
||||
@@ -300,5 +304,5 @@ export default function SignupPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
+51
-49
@@ -1,98 +1,102 @@
|
||||
import { FC, ReactElement, useEffect, useState, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useGitHubCallback } from '@imphnen-frontend-service/service';
|
||||
import { toast } from 'sonner';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { FC, ReactElement, useEffect, useState, useRef } from 'react'
|
||||
import { useGitHubCallback } from '@imphnen-frontend-service/service'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
const CallbackPage: FC = (): ReactElement => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync: exchangeGitHubCode } = useGitHubCallback();
|
||||
const [isProcessing, setIsProcessing] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const hasRunRef = useRef(false);
|
||||
export const Route = createFileRoute('/auth/callback')({
|
||||
component: CallbackPage,
|
||||
})
|
||||
|
||||
function CallbackPage(): ReactElement {
|
||||
const navigate = useNavigate()
|
||||
const { mutateAsync: exchangeGitHubCode } = useGitHubCallback()
|
||||
const [isProcessing, setIsProcessing] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const hasRunRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleCallback = async () => {
|
||||
if (hasRunRef.current) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
hasRunRef.current = true;
|
||||
hasRunRef.current = true
|
||||
|
||||
try {
|
||||
const hashParams = new URLSearchParams(
|
||||
globalThis.location.hash.substring(1)
|
||||
);
|
||||
const urlParams = new URLSearchParams(globalThis.location.search);
|
||||
)
|
||||
const urlParams = new URLSearchParams(globalThis.location.search)
|
||||
|
||||
const type = hashParams.get('type') || urlParams.get('type');
|
||||
const type = hashParams.get('type') || urlParams.get('type')
|
||||
const accessToken =
|
||||
hashParams.get('access_token') || urlParams.get('access_token');
|
||||
hashParams.get('access_token') || urlParams.get('access_token')
|
||||
|
||||
console.log('[Callback] Params:', {
|
||||
type,
|
||||
accessToken: !!accessToken,
|
||||
hash: globalThis.location.hash,
|
||||
search: globalThis.location.search,
|
||||
});
|
||||
})
|
||||
|
||||
if (accessToken) {
|
||||
setIsProcessing(false);
|
||||
setIsProcessing(false)
|
||||
|
||||
if (type === 'recovery' || type === 'magiclink') {
|
||||
toast.success('Email verified! Please set your new password.');
|
||||
navigate('/auth/reset-password?access_token=' + accessToken);
|
||||
return;
|
||||
toast.success('Email verified! Please set your new password.')
|
||||
navigate({ to: '/auth/reset-password', search: { access_token: accessToken } })
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'signup' || type === 'email_confirmation') {
|
||||
toast.success(
|
||||
'Email verified successfully! Please log in to continue.'
|
||||
);
|
||||
navigate('/auth/login');
|
||||
return;
|
||||
)
|
||||
navigate({ to: '/auth/login' })
|
||||
return
|
||||
}
|
||||
|
||||
toast.success('Email verified! Please set your new password.');
|
||||
navigate('/auth/reset-password?access_token=' + accessToken);
|
||||
return;
|
||||
toast.success('Email verified! Please set your new password.')
|
||||
navigate({ to: '/auth/reset-password', search: { access_token: accessToken } })
|
||||
return
|
||||
}
|
||||
|
||||
const code = urlParams.get('code');
|
||||
const code = urlParams.get('code')
|
||||
|
||||
if (!code) {
|
||||
throw new Error('No authorization code received');
|
||||
throw new Error('No authorization code received')
|
||||
}
|
||||
|
||||
const result = await exchangeGitHubCode({ code });
|
||||
const result = await exchangeGitHubCode({ code })
|
||||
|
||||
toast.success('Login successful!');
|
||||
setIsProcessing(false);
|
||||
toast.success('Login successful!')
|
||||
setIsProcessing(false)
|
||||
|
||||
if (result.user.location) {
|
||||
globalThis.location.replace('/dashboard');
|
||||
globalThis.location.replace('/dashboard')
|
||||
} else {
|
||||
globalThis.location.replace('/onboarding/user');
|
||||
globalThis.location.replace('/onboarding/user')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Callback] Error:', err);
|
||||
setError((err as Error).message);
|
||||
setIsProcessing(false);
|
||||
toast.error('An error occurred during login');
|
||||
console.error('[Callback] Error:', err)
|
||||
setError((err as Error).message)
|
||||
setIsProcessing(false)
|
||||
toast.error('An error occurred during login')
|
||||
|
||||
setTimeout(() => {
|
||||
navigate('/auth/login');
|
||||
}, 3000);
|
||||
navigate({ to: '/auth/login' })
|
||||
}, 3000)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleCallback();
|
||||
handleCallback()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
if (error) {
|
||||
const isPrivateEmailError =
|
||||
error.toLowerCase().includes('failed to create user') ||
|
||||
error.toLowerCase().includes('email') ||
|
||||
error.toLowerCase().includes('user record');
|
||||
error.toLowerCase().includes('user record')
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50 px-4">
|
||||
@@ -152,7 +156,7 @@ const CallbackPage: FC = (): ReactElement => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -165,7 +169,5 @@ const CallbackPage: FC = (): ReactElement => {
|
||||
<p className="text-gray-600">Please wait</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallbackPage;
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user