tahamajs/IE / Projects /front /src /components /project /ProjectSettings.tsx
tahamajs's picture
download
raw
11.2 kB
// src/components/project/ProjectSettings.tsx
import { useState } from "react";
import { Save, AlertTriangle, Globe, Lock, Users, Archive, Trash2, X, Plus, Loader2, Settings } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
interface ProjectSettingsProps {
project: {
id: number;
name: string;
description: string;
status: string;
visibility: string;
tags: string[];
};
onUpdate: (updates: any) => void;
onArchive: () => void;
onDelete: () => void;
}
const VISIBILITY_OPTIONS = [
{ value: "private", label: "Private", desc: "Only invited members", icon: Lock, style: "text-red-600 bg-red-50 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800/50" },
{ value: "team", label: "Team", desc: "Organization members", icon: Users, style: "text-amber-600 bg-amber-50 border-amber-200 dark:bg-amber-900/20 dark:text-amber-400 dark:border-amber-800/50" },
{ value: "public", label: "Public", desc: "Anyone can view", icon: Globe, style: "text-emerald-600 bg-emerald-50 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-400 dark:border-emerald-800/50" },
];
export function ProjectSettings({ project, onUpdate, onArchive, onDelete }: ProjectSettingsProps) {
const [name, setName] = useState(project.name);
const [description, setDescription] = useState(project.description || "");
const [visibility, setVisibility] = useState(project.visibility);
const [status, setStatus] = useState(project.status);
const [tags, setTags] = useState<string[]>(project.tags || []);
const [newTag, setNewTag] = useState("");
const [saving, setSaving] = useState(false);
const handleAddTag = () => {
const tag = newTag.trim().toLowerCase().replace(/\s+/g, "-");
if (tag && !tags.includes(tag) && tags.length < 10) {
setTags([...tags, tag]);
setNewTag("");
}
};
const handleSave = async () => {
if (!name.trim()) return;
setSaving(true);
try {
await onUpdate({ name: name.trim(), description: description.trim(), visibility, status, tags });
} catch {
// error handled by parent
} finally {
setSaving(false);
}
};
return (
<div className="space-y-6 max-w-2xl">
<div className="flex items-center gap-2 mb-6">
<Settings className="w-5 h-5 text-primary" />
<h3 className="text-base font-bold text-gray-900 dark:text-white">Project Settings</h3>
</div>
{/* Basic Info */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white/60 dark:bg-gray-900/60 backdrop-blur-md rounded-2xl border border-white/30 dark:border-gray-700/50 p-6 shadow-sm space-y-5"
>
<h4 className="text-sm font-bold text-gray-700 dark:text-gray-300 uppercase tracking-wider">Basic Info</h4>
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
Project Name <span className="text-red-500">*</span>
</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
maxLength={100}
className="w-full px-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-white/80 dark:bg-gray-800/80 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent transition"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">Description</label>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
rows={4}
maxLength={1000}
className="w-full px-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-white/80 dark:bg-gray-800/80 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent transition resize-none"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Status</label>
<div className="flex gap-2 flex-wrap">
{["active", "completed", "archived"].map(s => (
<button
key={s}
type="button"
onClick={() => setStatus(s)}
className={`px-4 py-2 rounded-xl text-sm font-medium border transition-all capitalize ${
status === s
? "bg-primary text-white border-primary shadow-sm shadow-primary/20"
: "border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:border-primary/40"
}`}
>
{s}
</button>
))}
</div>
</div>
</motion.div>
{/* Visibility */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.05 }}
className="bg-white/60 dark:bg-gray-900/60 backdrop-blur-md rounded-2xl border border-white/30 dark:border-gray-700/50 p-6 shadow-sm"
>
<h4 className="text-sm font-bold text-gray-700 dark:text-gray-300 uppercase tracking-wider mb-4">Visibility</h4>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{VISIBILITY_OPTIONS.map(opt => {
const Icon = opt.icon;
const selected = visibility === opt.value;
return (
<button
key={opt.value}
type="button"
onClick={() => setVisibility(opt.value)}
className={`p-4 rounded-2xl border-2 text-left transition-all ${
selected ? `${opt.style} ring-2 ring-offset-1 ring-current` : "border-gray-200 dark:border-gray-700 bg-white/50 dark:bg-gray-800/50 hover:bg-white dark:hover:bg-gray-800"
}`}
>
<Icon className="w-5 h-5 mb-2" />
<div className="font-bold text-sm capitalize">{opt.label}</div>
<div className="text-xs opacity-70 mt-0.5">{opt.desc}</div>
</button>
);
})}
</div>
</motion.div>
{/* Tags */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-white/60 dark:bg-gray-900/60 backdrop-blur-md rounded-2xl border border-white/30 dark:border-gray-700/50 p-6 shadow-sm"
>
<h4 className="text-sm font-bold text-gray-700 dark:text-gray-300 uppercase tracking-wider mb-4">Tags</h4>
<div className="flex gap-2 mb-3">
<input
type="text"
value={newTag}
onChange={e => setNewTag(e.target.value)}
onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); handleAddTag(); } }}
placeholder="Add tag..."
disabled={tags.length >= 10}
className="flex-1 px-3 py-2 rounded-xl border border-gray-200 dark:border-gray-700 bg-white/80 dark:bg-gray-800/80 text-sm focus:ring-2 focus:ring-primary focus:border-transparent transition"
/>
<button
type="button"
onClick={handleAddTag}
disabled={!newTag.trim() || tags.length >= 10}
className="p-2.5 rounded-xl bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-40 transition"
>
<Plus className="w-4 h-4" />
</button>
</div>
<div className="flex flex-wrap gap-2">
<AnimatePresence>
{tags.map(tag => (
<motion.span
key={tag}
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
className="flex items-center gap-1.5 px-3 py-1 text-xs bg-primary/10 text-primary border border-primary/20 rounded-full"
>
#{tag}
<button type="button" onClick={() => setTags(tags.filter(t => t !== tag))} className="hover:text-red-500 transition-colors">
<X className="w-3 h-3" />
</button>
</motion.span>
))}
</AnimatePresence>
</div>
</motion.div>
{/* Save Button */}
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.15 }}>
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={handleSave}
disabled={saving || !name.trim()}
className="w-full sm:w-auto flex items-center justify-center gap-2 px-8 py-3.5 bg-gradient-to-r from-primary to-indigo-600 text-white rounded-2xl font-bold shadow-lg shadow-primary/20 hover:shadow-primary/40 disabled:opacity-50 transition-all"
>
{saving ? <><Loader2 className="w-5 h-5 animate-spin" /> Saving...</> : <><Save className="w-5 h-5" /> Save Settings</>}
</motion.button>
</motion.div>
{/* Danger Zone */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-red-50/60 dark:bg-red-900/10 backdrop-blur-md rounded-2xl border border-red-200 dark:border-red-800/50 p-6"
>
<h4 className="text-sm font-bold text-red-700 dark:text-red-400 uppercase tracking-wider mb-4 flex items-center gap-2">
<AlertTriangle className="w-4 h-4" /> Danger Zone
</h4>
<div className="space-y-4">
<div className="flex items-center justify-between flex-wrap gap-4">
<div>
<p className="font-semibold text-red-700 dark:text-red-400 text-sm">Archive Project</p>
<p className="text-xs text-red-600/70 dark:text-red-500/70 mt-0.5">Move to archive. You can restore it later.</p>
</div>
<button
onClick={onArchive}
className="flex items-center gap-2 px-4 py-2 rounded-xl border border-red-300 dark:border-red-700 text-red-700 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/30 transition text-sm font-medium"
>
<Archive className="w-4 h-4" /> Archive
</button>
</div>
<div className="border-t border-red-200 dark:border-red-800/50 pt-4 flex items-center justify-between flex-wrap gap-4">
<div>
<p className="font-semibold text-red-700 dark:text-red-400 text-sm">Delete Project</p>
<p className="text-xs text-red-600/70 dark:text-red-500/70 mt-0.5">Permanently delete all data. Cannot be undone.</p>
</div>
<button
onClick={onDelete}
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-red-500 text-white hover:bg-red-600 transition text-sm font-medium shadow-sm shadow-red-500/20"
>
<Trash2 className="w-4 h-4" /> Delete Forever
</button>
</div>
</div>
</motion.div>
</div>
);
}

Xet Storage Details

Size:
11.2 kB
·
Xet hash:
c95a9c91af8872ea81b7ee74317bb5de6ec6fbf77cdd9af85e4062fc5e31b04c

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.