replace modals with a centralized component

This commit is contained in:
2026-03-12 18:03:04 +08:00
parent 94489a7769
commit 1ca65e2670
4 changed files with 335 additions and 259 deletions

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect } from "react";
import { X, Search, Trash2, UserPlus, Loader2, AlertCircle } from "lucide-react";
import { Search, Trash2, UserPlus, Loader2, AlertCircle } from "lucide-react";
import { searchUserByExactMobile, type SearchedUser } from "../api/users";
// Adjust the import path for your useLanguage hook if necessary
import { useTranslation } from "../hooks/useTranslation";
import { Modal } from "./Modal";
export interface WorkspaceMemberInput extends SearchedUser {
role: "admin" | "member";
@@ -87,22 +87,33 @@ export const CreateWorkspaceModal: React.FC<CreateWorkspaceModalProps> = ({
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm transition-opacity" dir={isFa ? "rtl" : "ltr"}>
<div className="w-full max-w-lg bg-white dark:bg-slate-900 rounded-xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
<div className="flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-800">
<h2 className="text-lg font-semibold text-slate-800 dark:text-slate-100">
{t.workspace?.createNew || "Create New Workspace"}
</h2>
<Modal
isOpen={isOpen}
onClose={onClose}
title={t.workspace?.createNew || "Create New Workspace"}
isFa={isFa}
footer={
<>
<button
type="button"
onClick={onClose}
className="p-1.5 rounded-md text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
disabled={isLoading}
className="px-4 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg transition-colors disabled:opacity-50"
>
<X className="w-5 h-5" />
{t.workspace?.cancel || "Cancel"}
</button>
</div>
<button
type="submit"
onClick={handleSubmit}
disabled={isLoading || !name.trim()}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50 flex items-center gap-2"
>
{isLoading && <Loader2 className="w-4 h-4 animate-spin" />}
{isLoading ? (t.workspace?.creating || "Creating...") : (t.workspace?.submit || "Create")}
</button>
</>
}
>
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-5 space-y-5">
<div className="space-y-1.5">
<label className="text-sm font-medium text-slate-700 dark:text-slate-300">
@@ -236,29 +247,6 @@ export const CreateWorkspaceModal: React.FC<CreateWorkspaceModalProps> = ({
</div>
</form>
<div className="p-4 border-t border-slate-200 dark:border-slate-800 flex justify-end gap-3 bg-slate-50 dark:bg-slate-800/50">
<button
type="button"
onClick={onClose}
disabled={isLoading}
className="px-4 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg transition-colors disabled:opacity-50"
>
{t.workspace?.cancel || "Cancel"}
</button>
<button
type="submit"
onClick={handleSubmit}
disabled={isLoading || !name.trim()}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50 flex items-center gap-2"
>
{isLoading && <Loader2 className="w-4 h-4 animate-spin" />}
{isLoading
? (t.workspace?.creating || "Creating...")
: (t.workspace?.submit || "Create Workspace")}
</button>
</div>
</div>
</div>
);
};
</Modal>
);
}

67
src/components/Modal.tsx Normal file
View File

@@ -0,0 +1,67 @@
import React, { useEffect } from "react";
import { X } from "lucide-react";
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
footer?: React.ReactNode;
maxWidth?: string;
isFa?: boolean;
}
export const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children,
footer,
maxWidth = "max-w-lg",
}) => {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "unset";
}
return () => {
document.body.style.overflow = "unset";
};
}, [isOpen]);
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
onClick={onClose}
>
<div
className={`bg-white dark:bg-slate-900 rounded-2xl shadow-xl w-full ${maxWidth} overflow-hidden`}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-800 shrink-0">
<h2 className="text-lg font-semibold text-slate-800 dark:text-slate-100">
{title}
</h2>
<button
type="button"
onClick={onClose}
className="p-1.5 rounded-md text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-5">{children}</div>
{footer && (
<div className="p-4 border-t border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-800/50 shrink-0 flex justify-end gap-3">
{footer}
</div>
)}
</div>
</div>
);
};

View File

@@ -15,12 +15,21 @@ export function Navbar() {
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
const [user, setUser] = useState<any>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
const isFa = lang === 'fa';
const [isDarkMode, setIsDarkMode] = useState(() =>
document.documentElement.classList.contains('dark')
)
const [isDarkMode, setIsDarkMode] = useState(() => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) return savedTheme === 'dark';
return document.documentElement.classList.contains('dark');
});
const isFa = lang === 'fa'
useEffect(() => {
if (isDarkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [isDarkMode]);
useEffect(() => {
const fetchUser = async () => {
@@ -67,10 +76,10 @@ export function Navbar() {
}
const toggleTheme = () => {
const isDark = document.documentElement.classList.toggle('dark')
setIsDarkMode(isDark)
localStorage.setItem('theme', isDark ? 'dark' : 'light')
}
const newThemeState = !isDarkMode;
setIsDarkMode(newThemeState);
localStorage.setItem('theme', newThemeState ? 'dark' : 'light');
};
const toggleLanguage = () => {
const newLang = isFa ? 'en' : 'fa'
@@ -84,13 +93,13 @@ export function Navbar() {
return (
<>
<header className="border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 px-6 py-4 flex items-center justify-between transition-colors">
<header className="border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 px-8 py-6 flex items-center justify-between transition-colors">
<div
className="flex items-center gap-2 cursor-pointer"
onClick={() => navigate("/")}
>
<span className="relative z-20 flex items-center gap-2 font-bold text-xl tracking-tight text-slate-900 dark:text-slate-50">
<Command className="h-6 w-6" />
<Command className="h-7 w-7" />
Qlockify
</span>
</div>
@@ -101,7 +110,7 @@ export function Navbar() {
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="w-10 h-10 rounded-full overflow-hidden border-2 border-slate-200 dark:border-slate-700 hover:border-blue-500 dark:hover:border-blue-500 transition-all focus:outline-none"
className="w-12 h-12 rounded-full overflow-hidden border-2 border-slate-200 dark:border-slate-700 hover:border-blue-500 dark:hover:border-blue-500 transition-all focus:outline-none"
>
{user.profile_picture ? (
<img

View File

@@ -11,6 +11,7 @@ import { Button } from "../components/ui/button"
import { Camera, Edit2, Trash2, User as UserIcon, UploadCloud } from "lucide-react"
import JalaliDatePicker from "../components/ui/JalaliDatePicker"
import { toast } from "sonner"
import { Modal } from "../components/Modal"
export interface UserProfile {
id?: string;
@@ -261,17 +262,32 @@ export default function Profile() {
{/* Edit Profile Modal */}
{isEditModalOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm px-4"
onClick={() => !isSaving && setIsEditModalOpen(false)}
<Modal
isOpen={isEditModalOpen}
isFa={isFa}
onClose={() => setIsEditModalOpen(false)}
title={t.profile?.title || "Edit Profile"}
maxWidth="max-w-lg"
footer={
<>
<button
type="button"
onClick={() => setIsEditModalOpen(false)}
className="px-4 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg"
>
<div
className="w-full max-w-lg rounded-xl bg-white p-6 shadow-lg dark:bg-slate-900 border dark:border-slate-800"
onClick={(e) => e.stopPropagation()}
{t.cancel || "Cancel"}
</button>
<button
onClick={handleSaveProfile}
disabled={isSaving}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg disabled:opacity-50"
>
<h2 className="mb-4 text-xl font-bold text-slate-900 dark:text-white">
{t.profile?.editInfo || 'Edit Profile'}
</h2>
{isSaving ? "Saving..." : t.profile?.save || "Save"}
</button>
</>
}
>
<div className="space-y-4">
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
@@ -323,35 +339,29 @@ export default function Profile() {
/>
</div>
</div>
<div className="mt-6 flex justify-end gap-3">
<Button variant="outline" onClick={() => setIsEditModalOpen(false)} disabled={isSaving}>
{t.profile?.cancel || t.cancel || 'Cancel'}
</Button>
<Button onClick={handleSaveProfile} disabled={isSaving}>
{isSaving ? '...' : (t.profile?.save || 'Save')}
</Button>
</div>
</div>
</div>
</Modal>
)}
{/* Profile Picture Modal */}
{isPicModalOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm px-4"
onClick={() => !isSaving && setIsPicModalOpen(false)}
<Modal
isOpen={isPicModalOpen}
onClose={() => !isSaving && setIsPicModalOpen(false)}
title={t.profile?.changePicture || 'Profile Picture'}
maxWidth="max-w-sm"
footer={
<Button
variant="outline"
onClick={() => setIsPicModalOpen(false)}
disabled={isSaving}
>
<div
className="w-full max-w-sm rounded-xl bg-white p-6 shadow-lg dark:bg-slate-900 border dark:border-slate-800"
onClick={(e) => e.stopPropagation()}
{t.profile?.cancel || t.cancel || 'Cancel'}
</Button>
}
>
<h2 className="mb-4 text-xl font-bold text-slate-900 dark:text-white">
{t.profile?.changePicture || 'Profile Picture'}
</h2>
<div className="space-y-4">
{/* Drag and Drop Zone */}
<div
onDragEnter={handleDrag}
@@ -395,26 +405,28 @@ export default function Profile() {
</div>
<div className="flex flex-col gap-2 mt-4">
<Button onClick={handlePictureUpload} disabled={!selectedFile || isSaving} className="w-full">
<Button
onClick={handlePictureUpload}
disabled={!selectedFile || isSaving}
className="w-full"
>
{t.profile?.upload || 'Upload'}
</Button>
{user.profile_picture && (
<Button variant="destructive" onClick={handleDeletePicture} disabled={isSaving} className="w-full flex items-center justify-center gap-2">
{user?.profile_picture && (
<Button
variant="destructive"
onClick={handleDeletePicture}
disabled={isSaving}
className="w-full flex items-center justify-center gap-2"
>
<Trash2 className="h-4 w-4" />
{t.profile?.remove || "Remove"}
</Button>
)}
</div>
</div>
<div className="mt-6 flex justify-end">
<Button variant="outline" onClick={() => setIsPicModalOpen(false)} disabled={isSaving}>
{t.profile?.cancel || t.cancel || 'Cancel'}
</Button>
</div>
</div>
</div>
</Modal>
)}
</div>