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
@@ -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}