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:
co-authored by
Claude Opus 4.6
parent
f68d97188c
commit
3f4461c65c
@@ -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') ||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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!');
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user