chore: upgrade dependencies, restructure shared libs, and fix UI

- Upgrade Nx 22.1.1 → 22.6.3 and all patch/minor dependencies
- Restructure shared libs: move business logic from utils to service
- Consolidate shadcn-ui into ui lib with atomic design pattern
- Fix container centering for landing app (Tailwind v4 compatibility)
- Fix button styling by updating @source directive in globals.css
- Fix SiCss3 → SiCss rename in react-icons 5.6
- Fix duplicate useSession export conflict
- Remove dead code, comments, and unused files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-03-31 01:44:17 +07:00
co-authored by Claude Opus 4.6
parent f68d97188c
commit 3f4461c65c
231 changed files with 6062 additions and 39166 deletions
@@ -221,7 +221,6 @@ export default function CampaignsPage() {
<DataTable data={campaigns} columns={columns} pageSize={10} />
)}
{/* Create Campaign Modal */}
{showCreateModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md">
@@ -31,7 +31,6 @@ export default function UsersPage() {
const handleUpdateRole = async (userId: string, currentRole: string) => {
if (editingUserId === userId) {
// Save the role
try {
await userService.updateUserRole(userId, selectedRole);
setEditingUserId(null);
@@ -42,7 +41,6 @@ export default function UsersPage() {
alert('Failed to update user role');
}
} else {
// Start editing
setEditingUserId(userId);
setSelectedRole(currentRole);
}
@@ -18,7 +18,6 @@ const CallbackPage: FC = (): ReactElement => {
hasRunRef.current = true;
try {
// Check URL hash for Supabase email confirmation callback
const hashParams = new URLSearchParams(
globalThis.location.hash.substring(1)
);
@@ -28,7 +27,6 @@ const CallbackPage: FC = (): ReactElement => {
const accessToken =
hashParams.get('access_token') || urlParams.get('access_token');
// Debug: log what we received
console.log('[Callback] Params:', {
type,
accessToken: !!accessToken,
@@ -36,19 +34,15 @@ const CallbackPage: FC = (): ReactElement => {
search: globalThis.location.search,
});
// Handle Supabase email callbacks (has access_token in hash or query)
// This includes: signup confirmation, email confirmation, password recovery
if (accessToken) {
setIsProcessing(false);
// Password recovery - type is 'recovery' or we have access_token from reset email
if (type === 'recovery' || type === 'magiclink') {
toast.success('Email verified! Please set your new password.');
navigate('/auth/reset-password?access_token=' + accessToken);
return;
}
// Signup/Email confirmation
if (type === 'signup' || type === 'email_confirmation') {
toast.success(
'Email verified successfully! Please log in to continue.'
@@ -57,27 +51,22 @@ const CallbackPage: FC = (): ReactElement => {
return;
}
// If we have access_token but unknown type, assume it's password recovery
// (Supabase sometimes sends without explicit type)
toast.success('Email verified! Please set your new password.');
navigate('/auth/reset-password?access_token=' + accessToken);
return;
}
// Get the code from URL query params (GitHub OAuth)
const code = urlParams.get('code');
if (!code) {
throw new Error('No authorization code received');
}
// Exchange the code for tokens using backend API (GitHub OAuth)
const result = await exchangeGitHubCode({ code });
toast.success('Login successful!');
setIsProcessing(false);
// Check if user has completed onboarding (has location)
if (result.user.location) {
globalThis.location.replace('/dashboard');
} else {
@@ -100,7 +89,6 @@ const CallbackPage: FC = (): ReactElement => {
}, []);
if (error) {
// Check if error is related to private email
const isPrivateEmailError =
error.toLowerCase().includes('failed to create user') ||
error.toLowerCase().includes('email') ||
+2 -14
View File
@@ -23,7 +23,6 @@ export default function LoginPage() {
}
}, [isAuthenticated, navigate]);
// Check for password reset tokens in URL and redirect to reset-password page
useEffect(() => {
const hashParams = new URLSearchParams(
globalThis.location.hash.substring(1)
@@ -34,13 +33,11 @@ export default function LoginPage() {
hashParams.get('access_token') || urlParams.get('access_token');
const type = hashParams.get('type') || urlParams.get('type');
// If we have an access_token, this is likely a password reset redirect that landed on the wrong page
if (accessToken) {
console.log(
'[Login] Detected access_token, redirecting to reset-password page'
);
// Check if it's a password recovery
if (type === 'recovery' || type === 'magiclink' || !type) {
toast.info('Redirecting to password reset...');
navigate('/auth/reset-password?access_token=' + accessToken);
@@ -76,22 +73,14 @@ export default function LoginPage() {
};
const handleGithubLogin = async () => {
// TODO: Implement GitHub login with new auth service if needed
toast.info('GitHub login coming soon');
};
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('/')}
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" />
Back to Homepage
</button>
</div> */}
{
}
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-2">
@@ -106,7 +95,6 @@ export default function LoginPage() {
</div>
)}
{/* Traditional Login Form */}
<form onSubmit={handleEmailLogin} className="space-y-4 mb-6">
<div>
<label
@@ -18,8 +18,6 @@ export default function ResetPasswordPage() {
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
useEffect(() => {
// Get the access_token from URL hash (Supabase sends it as hash fragment)
// or from query params (when redirected from callback page)
const hashParams = new URLSearchParams(
globalThis.location.hash.substring(1)
);
@@ -61,7 +59,6 @@ export default function ResetPasswordPage() {
toast.success('Password updated successfully!');
// Clear session and redirect to login
clearSession();
navigate('/auth/login');
} catch (err) {
@@ -62,7 +62,6 @@ export default function SignupPage() {
navigate('/');
} catch (err: any) {
console.error('[Signup] Email signup failed:', err);
// Construct a user-friendly error message
const errorMessage =
err.response?.data?.message || err.message || 'Signup failed';
setError(errorMessage);
@@ -1,6 +1,5 @@
import { api } from '../../auth/api/auth.service';
// Types
export interface Campaign {
id: string;
name: string;
@@ -1,6 +1,5 @@
import { api } from '../../auth/api/auth.service';
// Types
export interface User {
id: string;
email: string;
@@ -15,11 +15,8 @@ export const RequireAdmin = ({ children }: RequireAdminProps) => {
return <Navigate to="/login" state={{ from: location }} replace />;
}
// Check if user has admin role
// user.role is now an object { id, name, permissions }
const userRole = user?.role?.name;
if (userRole !== 'Admin' && userRole !== 'Super Admin') {
// Redirect non-admins to home
return <Navigate to="/" replace />;
}
@@ -9,7 +9,7 @@ export const AdminDashboard = () => {
return (
<div className="min-h-screen bg-slate-100 flex">
{/* Sidebar */}
<aside className="w-64 bg-white border-r border-slate-200 flex flex-col">
<div className="p-6 border-b border-slate-200">
<h1 className="text-xl font-bold text-slate-800">Admin Panel</h1>
@@ -55,7 +55,6 @@ export const AdminDashboard = () => {
</div>
</aside>
{/* Main Content */}
<main className="flex-1 p-8 overflow-auto">
<Outlet />
</main>
@@ -8,7 +8,7 @@ interface Campaign {
id: string;
name: string;
url: string;
image_url?: string; // QR code image URL if we want to show it
image_url?: string;
is_active: boolean;
created_at: string;
}
@@ -28,7 +28,6 @@ export const CampaignManagement = () => {
formState: { errors },
} = useForm<CreateCampaignInputs>();
// Fetch Campaigns
const {
data: campaigns,
isLoading,
@@ -41,7 +40,6 @@ export const CampaignManagement = () => {
},
});
// Create Campaign
const createMutation = useMutation({
mutationFn: async (data: CreateCampaignInputs) => {
await axios.post('http://localhost:8080/api/v1/campaigns', data);
@@ -57,7 +55,6 @@ export const CampaignManagement = () => {
},
});
// Activate Campaign
const activateMutation = useMutation({
mutationFn: async (id: string) => {
await axios.put(`http://localhost:8080/api/v1/campaigns/${id}/activate`);
@@ -65,13 +62,11 @@ export const CampaignManagement = () => {
onSuccess: () => {
toast.success('Campaign activated');
queryClient.invalidateQueries({ queryKey: ['campaigns'] });
// Also invalidate active QR for the main app
queryClient.invalidateQueries({ queryKey: ['active-campaign-qr'] });
},
onError: () => toast.error('Failed to activate campaign'),
});
// Delete Campaign
const deleteMutation = useMutation({
mutationFn: async (id: string) => {
await axios.delete(`http://localhost:8080/api/v1/campaigns/${id}`);
@@ -172,7 +167,6 @@ export const CampaignManagement = () => {
</table>
</div>
{/* Basic Create Modal */}
{isCreateModalOpen && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
@@ -13,7 +13,6 @@ interface User {
export const UserManagement = () => {
const queryClient = useQueryClient();
// Fetch Users
const {
data: users,
isLoading,
@@ -26,7 +25,6 @@ export const UserManagement = () => {
},
});
// Update Role
const updateRoleMutation = useMutation({
mutationFn: async ({ id, role }: { id: string; role: string }) => {
await axios.put(`http://localhost:8080/api/v1/users/${id}/role`, {
@@ -40,7 +38,6 @@ export const UserManagement = () => {
onError: () => toast.error('Failed to update user role'),
});
// Delete User
const deleteMutation = useMutation({
mutationFn: async (id: string) => {
await axios.delete(`http://localhost:8080/api/v1/users/${id}`);
@@ -87,7 +84,7 @@ export const UserManagement = () => {
role: e.target.value,
})
}
disabled={user.email === 'admin@demo.com'} // Prevent changing main admin role for safety in demo
disabled={user.email === 'admin@demo.com'}
className="bg-transparent border border-slate-300 rounded text-sm px-2 py-1 text-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="user">User</option>
@@ -1,9 +1,7 @@
import axios from 'axios';
// Define the base URL for the API
const API_URL = 'https://api-qr.imphnen.dev/api/v1';
// Create a configured axios instance
export const api = axios.create({
baseURL: API_URL,
headers: {
@@ -11,7 +9,6 @@ export const api = axios.create({
},
});
// Add interceptor to add token to requests
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
@@ -23,7 +20,6 @@ api.interceptors.request.use(
(error) => Promise.reject(error)
);
// Types
export interface LoginRequest {
email: string;
password: string;
@@ -35,7 +31,6 @@ export interface RegisterRequest {
password: string;
}
// Backend user response
interface BackendUser {
id: string;
email: string;
@@ -46,7 +41,6 @@ interface BackendUser {
updated_at: string;
}
// Frontend user type
export interface User {
id: string;
email: string;
@@ -58,7 +52,6 @@ export interface User {
};
}
// Backend auth response
interface BackendAuthResponse {
success: boolean;
message: string;
@@ -83,7 +76,6 @@ export interface AuthResponse {
};
}
// Helper to transform backend user to frontend user
const transformUser = (backendUser: BackendUser): User => {
return {
id: backendUser.id,
@@ -11,9 +11,6 @@ export const RequireAuth = ({ children }: RequireAuthProps) => {
const location = useLocation();
if (!isAuthenticated) {
// Redirect them to the /login page, but save the current location they were
// trying to go to when they were redirected. This allows us to send them
// along to that page after they login, which is a nicer user experience.
return <Navigate to="/login" state={{ from: location }} replace />;
}
@@ -3,9 +3,6 @@ import { useForm } from 'react-hook-form';
import { useAuthStore } from '../store/auth.store';
import { useNavigate, useLocation } from 'react-router-dom';
// Reusing UI components logic or standard HTML for now to keep it simple and dependency-free if UI lib issues arise
// But user mentioned shared UI libs, let's try to use standard Tailwind first to ensure speed.
interface LoginFormInputs {
email: string;
pass: string;
@@ -33,11 +30,9 @@ export const LoginPage = () => {
const onSubmit = async (data: LoginFormInputs) => {
const success = await login(data.email, data.pass);
if (success) {
// Get user from store to check role
const user = useAuthStore.getState().user;
const userRole = user?.role?.name;
// Redirect admin to admin dashboard
if (userRole === 'Admin' || userRole === 'Super Admin') {
navigate('/admin/campaigns', { replace: true });
} else {
@@ -73,7 +73,7 @@ export const useAuthStore = create<AuthState>()(
setUser: (user) => set({ user }),
}),
{
name: 'auth-storage', // name of the item in the storage (must be unique)
name: 'auth-storage',
partialize: (state) => ({
user: state.user,
token: state.token,
@@ -5,13 +5,11 @@ export const useActiveCampaignQR = () => {
return useQuery({
queryKey: ['active-campaign-qr'],
queryFn: async () => {
// Assuming backend is running on localhost:8080
// In production, this should be an env var or relative path if proxied
const response = await axios.get('http://localhost:8080/api/v1/campaigns/active/qr', {
responseType: 'blob',
});
return URL.createObjectURL(response.data);
},
staleTime: 1000 * 60 * 5, // 5 minutes
staleTime: 1000 * 60 * 5,
});
};
@@ -73,7 +73,7 @@ export const Dropzone: React.FC<DropzoneProps> = ({ onImageDropped }) => {
/>
<div className="space-y-2">
<div className="flex justify-center">
{/* Simple upload icon */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -17,7 +17,6 @@ export const WatermarkEditor: React.FC<WatermarkEditorProps> = ({
const containerRef = useRef<HTMLDivElement>(null);
const qrRef = useRef<HTMLDivElement>(null);
// State for QR code
const [position, setPosition] = useState({ x: 20, y: 20 });
const [size, setSize] = useState(100);
const [isDragging, setIsDragging] = useState(false);
@@ -26,14 +25,12 @@ export const WatermarkEditor: React.FC<WatermarkEditorProps> = ({
const [startResizePos, setStartResizePos] = useState({ x: 0, y: 0 });
const [startResizeSize, setStartResizeSize] = useState(100);
// Load image
useEffect(() => {
const url = URL.createObjectURL(imageFile);
setImageUrl(url);
return () => URL.revokeObjectURL(url);
}, [imageFile]);
// Drag handlers
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
@@ -58,11 +55,7 @@ export const WatermarkEditor: React.FC<WatermarkEditorProps> = ({
const newX = e.clientX - dragOffset.x;
const newY = e.clientY - dragOffset.y;
// Boundaries check (optional, but good UX)
if (containerRef.current) {
// const container = containerRef.current.getBoundingClientRect();
// Simple clamp? Or allow partial off-screen?
// Let's allow it to move freely within container
}
setPosition({ x: newX, y: newY });
@@ -70,7 +63,7 @@ export const WatermarkEditor: React.FC<WatermarkEditorProps> = ({
if (isResizing) {
const deltaX = e.clientX - startResizePos.x;
const newSize = Math.max(50, startResizeSize + deltaX); // Min size 50px
const newSize = Math.max(50, startResizeSize + deltaX);
setSize(newSize);
}
},
@@ -101,7 +94,7 @@ export const WatermarkEditor: React.FC<WatermarkEditorProps> = ({
try {
const canvas = await html2canvas(containerRef.current, {
useCORS: true, // Important for QR if from external URL
useCORS: true,
backgroundColor: null,
});
@@ -164,13 +157,11 @@ export const WatermarkEditor: React.FC<WatermarkEditorProps> = ({
src={qrCodeUrl}
alt="QR Code"
className="w-full h-full select-none pointer-events-none"
crossOrigin="anonymous" // Important for html2canvas
crossOrigin="anonymous"
/>
{/* Outline on hover/interaction */}
<div className="absolute inset-0 border-2 border-transparent group-hover:border-blue-400 group-active:border-blue-500 pointer-events-none rounded-sm transition-colors" />
{/* Resize handle */}
<div
className="absolute bottom-0 right-0 w-4 h-4 bg-blue-500 rounded-full cursor-nwse-resize opacity-0 group-hover:opacity-100 transition-opacity"
onMouseDown={handleResizeMouseDown}
-9
View File
@@ -9,7 +9,6 @@ import { useAuthStore } from './features/auth/store/auth.store';
import { Sidebar } from '../components/Sidebar';
import { MenuOutlined } from '@ant-design/icons';
// Helper to determine route types
const isPublicRoute = (pathname: string) => {
return pathname.startsWith('/auth') || pathname === '/auth/callback';
};
@@ -21,22 +20,16 @@ export default function RootLayout() {
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
useEffect(() => {
// If we're on a public route, no auth check needed
if (isPublicRoute(location.pathname)) {
return;
}
// AUTH CHECK
if (!isAuthenticated) {
// No session -> Redirect to login
navigate('/auth/login', { replace: true });
return;
}
}, [location.pathname, navigate, isAuthenticated]);
// RENDER LOGIC
// 1. Public Pages (Full Layout Control)
if (isPublicRoute(location.pathname)) {
return (
<>
@@ -46,7 +39,6 @@ export default function RootLayout() {
);
}
// 2. Protected Pages
if (!isAuthenticated) {
return null;
}
@@ -59,7 +51,6 @@ export default function RootLayout() {
/>
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
{/* Mobile Header */}
<header className="lg:hidden bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between sticky top-0 z-30">
<div className="flex items-center gap-3">
<button
+1 -1
View File
@@ -11,7 +11,7 @@ export default function HomePage() {
const handleImageDropped = (file: File) => {
setImageFile(file);
setGeneratedImage(null); // Reset previous result
setGeneratedImage(null);
toast.success('Image selected ready for generation!');
};