Files
task-manager/client/src/pages/settings.tsx
T
paul-nothaft 16c097b3f0 Add ability to manage task labels and create project templates
Refactors the settings page to include CRUD operations for task labels, integrates template creation navigation, and removes the appearance section.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/LNMKOrA
2025-10-24 10:21:30 +00:00

320 lines
12 KiB
TypeScript

import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery, useMutation } from '@tanstack/react-query';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Globe, Tag, Plus, Edit, Trash2, Layers } from 'lucide-react';
import { Label } from '@shared/schema';
import { queryClient, apiRequest } from '@/lib/queryClient';
import { useToast } from '@/hooks/use-toast';
interface SettingsProps {
onNavigateToTemplates: () => void;
}
export default function Settings({ onNavigateToTemplates }: SettingsProps) {
const { t, i18n } = useTranslation();
const { toast } = useToast();
const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false);
const [editingLabel, setEditingLabel] = useState<Label | null>(null);
const [labelName, setLabelName] = useState('');
const [labelColor, setLabelColor] = useState('#3B82F6');
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
localStorage.setItem('taskflow-language', lng);
console.log('Language changed to:', lng);
};
// Fetch labels
const { data: labels = [], isLoading: labelsLoading } = useQuery<Label[]>({
queryKey: ['/api/labels']
});
// Create label mutation
const createLabelMutation = useMutation({
mutationFn: (data: { name: string; color: string }) =>
apiRequest('POST', '/api/labels', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/labels'] });
setIsLabelDialogOpen(false);
setLabelName('');
setLabelColor('#3B82F6');
toast({
title: t('settings.labels.created'),
description: t('settings.labels.createdDescription'),
});
},
});
// Update label mutation
const updateLabelMutation = useMutation({
mutationFn: ({ id, ...data }: { id: string; name: string; color: string }) =>
apiRequest('PATCH', `/api/labels/${id}`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/labels'] });
setIsLabelDialogOpen(false);
setEditingLabel(null);
setLabelName('');
setLabelColor('#3B82F6');
toast({
title: t('settings.labels.updated'),
description: t('settings.labels.updatedDescription'),
});
},
});
// Delete label mutation
const deleteLabelMutation = useMutation({
mutationFn: (id: string) => apiRequest('DELETE', `/api/labels/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/labels'] });
toast({
title: t('settings.labels.deleted'),
description: t('settings.labels.deletedDescription'),
});
},
});
const handleSaveLabel = () => {
if (!labelName.trim()) return;
if (editingLabel) {
updateLabelMutation.mutate({
id: editingLabel.id,
name: labelName,
color: labelColor,
});
} else {
createLabelMutation.mutate({ name: labelName, color: labelColor });
}
};
const handleEditLabel = (label: Label) => {
setEditingLabel(label);
setLabelName(label.name);
setLabelColor(label.color);
setIsLabelDialogOpen(true);
};
const handleDeleteLabel = (id: string) => {
if (confirm(t('settings.labels.deleteConfirm'))) {
deleteLabelMutation.mutate(id);
}
};
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold" data-testid="text-settings-title">
{t('settings.title')}
</h1>
</div>
{/* Language Settings */}
<Card data-testid="card-language-settings">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Globe className="w-5 h-5" />
{t('settings.language.title')}
</CardTitle>
<CardDescription>
{t('settings.language.description')}
</CardDescription>
</CardHeader>
<CardContent>
<Select value={i18n.language} onValueChange={changeLanguage}>
<SelectTrigger className="w-full sm:w-64" data-testid="select-language">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="en" data-testid="option-language-en">
{t('settings.language.english')}
</SelectItem>
<SelectItem value="de" data-testid="option-language-de">
{t('settings.language.german')}
</SelectItem>
</SelectContent>
</Select>
</CardContent>
</Card>
{/* Labels Management */}
<Card data-testid="card-labels-management">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Tag className="w-5 h-5" />
{t('settings.labels.title')}
</CardTitle>
<CardDescription>
{t('settings.labels.description')}
</CardDescription>
</div>
<Dialog open={isLabelDialogOpen} onOpenChange={setIsLabelDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm" data-testid="button-create-label">
<Plus className="w-4 h-4 mr-2" />
{t('settings.labels.createLabel')}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingLabel ? t('settings.labels.editLabel') : t('settings.labels.createNewLabel')}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Input
placeholder={t('settings.labels.labelNamePlaceholder')}
value={labelName}
onChange={(e) => setLabelName(e.target.value)}
data-testid="input-label-name"
/>
</div>
<div>
<div className="flex items-center gap-3">
<input
type="color"
value={labelColor}
onChange={(e) => setLabelColor(e.target.value)}
className="w-12 h-8 rounded border cursor-pointer"
data-testid="input-label-color"
/>
<Input
value={labelColor}
onChange={(e) => setLabelColor(e.target.value)}
placeholder="#3B82F6"
className="flex-1"
data-testid="input-label-color-text"
/>
</div>
</div>
<div className="flex gap-3 pt-2">
<Button
variant="outline"
onClick={() => {
setIsLabelDialogOpen(false);
setEditingLabel(null);
setLabelName('');
setLabelColor('#3B82F6');
}}
className="flex-1"
data-testid="button-cancel-label"
>
{t('settings.labels.cancel')}
</Button>
<Button
onClick={handleSaveLabel}
disabled={!labelName.trim() || createLabelMutation.isPending || updateLabelMutation.isPending}
className="flex-1"
data-testid="button-save-label"
>
{editingLabel ? t('settings.labels.update') : t('settings.labels.create')}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
{labelsLoading ? (
<div className="flex items-center justify-center p-8">
<div className="text-muted-foreground">{t('settings.labels.loading')}</div>
</div>
) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{labels.map((label) => (
<Card
key={label.id}
className="p-4 hover-elevate active-elevate-2"
style={{ borderLeft: `4px solid ${label.color}` }}
data-testid={`label-${label.id}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div
className="w-4 h-4 rounded"
style={{ backgroundColor: label.color }}
/>
<span className="font-medium text-sm" data-testid={`text-label-name-${label.id}`}>
{label.name}
</span>
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => handleEditLabel(label)}
className="w-6 h-6"
data-testid={`button-edit-${label.id}`}
>
<Edit className="w-3 h-3" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteLabel(label.id)}
className="w-6 h-6 text-destructive hover:text-destructive"
data-testid={`button-delete-${label.id}`}
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
</Card>
))}
{labels.length === 0 && (
<div className="col-span-full flex flex-col items-center justify-center p-8 text-center">
<Tag className="w-12 h-12 text-muted-foreground mb-4" />
<h3 className="font-medium text-muted-foreground mb-2">{t('settings.labels.noLabels')}</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('settings.labels.noLabelsDescription')}
</p>
<Button
variant="outline"
onClick={() => setIsLabelDialogOpen(true)}
data-testid="button-create-first-label"
>
<Plus className="w-4 h-4 mr-2" />
{t('settings.labels.createLabel')}
</Button>
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* Project Templates */}
<Card data-testid="card-project-templates">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Layers className="w-5 h-5" />
{t('settings.templates.title')}
</CardTitle>
<CardDescription>
{t('settings.templates.description')}
</CardDescription>
</CardHeader>
<CardContent>
<Button
onClick={onNavigateToTemplates}
data-testid="button-manage-templates"
>
<Layers className="w-4 h-4 mr-2" />
{t('settings.templates.manageTemplates')}
</Button>
</CardContent>
</Card>
</div>
);
}