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
This commit is contained in:
paul-nothaft
2025-10-24 10:21:30 +00:00
parent 56709ce8ca
commit 16c097b3f0
5 changed files with 296 additions and 31 deletions
+1 -1
View File
@@ -348,7 +348,7 @@ function App() {
);
case 'settings':
return <Settings />;
return <Settings onNavigateToTemplates={() => setCurrentTab('templates')} />;
default:
return (
+21 -5
View File
@@ -172,15 +172,31 @@
"english": "Englisch (English)",
"german": "Deutsch"
},
"appearance": {
"title": "Erscheinungsbild",
"description": "Passe das Aussehen an",
"themeToggle": "Thema-Umschalter ist in der Kopfzeile verfügbar"
"labels": {
"title": "Aufgabenlabels",
"description": "Erstellen und verwalten Sie Labels, um Ihre Aufgaben zu organisieren",
"createLabel": "Label erstellen",
"createNewLabel": "Neues Label erstellen",
"editLabel": "Label bearbeiten",
"labelNamePlaceholder": "Label-Name",
"cancel": "Abbrechen",
"create": "Erstellen",
"update": "Aktualisieren",
"loading": "Labels werden geladen...",
"noLabels": "Noch keine Labels",
"noLabelsDescription": "Erstellen Sie Ihr erstes Label, um Ihre Aufgaben nach Farbe und Kategorie zu organisieren.",
"deleteConfirm": "Sind Sie sicher, dass Sie dieses Label löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"created": "Label erstellt",
"createdDescription": "Ihr Label wurde erfolgreich erstellt.",
"updated": "Label aktualisiert",
"updatedDescription": "Ihr Label wurde erfolgreich aktualisiert.",
"deleted": "Label gelöscht",
"deletedDescription": "Ihr Label wurde erfolgreich gelöscht."
},
"templates": {
"title": "Projektvorlagen",
"description": "Verwende Vorlagen zum schnellen Erstellen von Projekten",
"accessInfo": "Zugriff auf Vorlagen über das Navigationsmenü"
"manageTemplates": "Vorlagen verwalten"
}
},
"projectTemplate": {
+21 -5
View File
@@ -172,15 +172,31 @@
"english": "English",
"german": "German (Deutsch)"
},
"appearance": {
"title": "Appearance",
"description": "Customize the look and feel",
"themeToggle": "Theme toggle available in the header"
"labels": {
"title": "Task Labels",
"description": "Create and manage labels to organize your tasks",
"createLabel": "Create Label",
"createNewLabel": "Create New Label",
"editLabel": "Edit Label",
"labelNamePlaceholder": "Label name",
"cancel": "Cancel",
"create": "Create",
"update": "Update",
"loading": "Loading labels...",
"noLabels": "No labels yet",
"noLabelsDescription": "Create your first label to organize your tasks by color and category.",
"deleteConfirm": "Are you sure you want to delete this label? This action cannot be undone.",
"created": "Label created",
"createdDescription": "Your label has been created successfully.",
"updated": "Label updated",
"updatedDescription": "Your label has been updated successfully.",
"deleted": "Label deleted",
"deletedDescription": "Your label has been deleted successfully."
},
"templates": {
"title": "Project Templates",
"description": "Use templates to quickly create projects",
"accessInfo": "Access templates from the navigation menu"
"manageTemplates": "Manage Templates"
}
},
"projectTemplate": {
+253 -16
View File
@@ -1,10 +1,28 @@
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 { Globe } from 'lucide-react';
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';
export default function Settings() {
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);
@@ -12,6 +30,83 @@ export default function Settings() {
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>
@@ -48,33 +143,175 @@ export default function Settings() {
</CardContent>
</Card>
{/* Appearance Settings */}
<Card>
{/* Labels Management */}
<Card data-testid="card-labels-management">
<CardHeader>
<CardTitle>{t('settings.appearance.title')}</CardTitle>
<CardDescription>
{t('settings.appearance.description')}
</CardDescription>
<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>
<p className="text-sm text-muted-foreground">
{t('settings.appearance.themeToggle')}
</p>
{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>
<Card data-testid="card-project-templates">
<CardHeader>
<CardTitle>{t('settings.templates.title')}</CardTitle>
<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>
<p className="text-sm text-muted-foreground">
{t('settings.templates.accessInfo')}
</p>
<Button
onClick={onNavigateToTemplates}
data-testid="button-manage-templates"
>
<Layers className="w-4 h-4 mr-2" />
{t('settings.templates.manageTemplates')}
</Button>
</CardContent>
</Card>
</div>