feat: add Settings page with hijri seasons CRUD

- Server: seasons CRUD routes + generic NocoDB helpers
- Client: Settings page at /settings with inline add/edit/delete
- Seasons stored in NocoDB Seasons table
- Vite proxy: /api/seasons routed to Express server
- Nav links added (desktop + mobile)
- Locale keys for EN + AR
- Seasons loaded non-blocking on app mount

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-31 16:03:50 +03:00
parent 1dd216f933
commit ef48372033
11 changed files with 477 additions and 6 deletions
+169
View File
@@ -0,0 +1,169 @@
import React, { useState, useEffect } from 'react';
import { useLanguage } from '../contexts/LanguageContext';
import { fetchSeasons, createSeason, updateSeason, deleteSeason } from '../services/seasonsService';
import type { Season } from '../types';
const DEFAULT_COLORS = ['#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#ec4899'];
interface SeasonRowProps {
season: Season;
onSave: (id: number, data: Partial<Season>) => Promise<void>;
onDelete: (id: number) => Promise<void>;
}
function SeasonRow({ season, onSave, onDelete }: SeasonRowProps) {
const [editing, setEditing] = useState(false);
const [form, setForm] = useState(season);
const handleSave = async () => {
await onSave(season.Id!, form);
setEditing(false);
};
if (!editing) {
return (
<tr>
<td>
<span className="season-chip" style={{ backgroundColor: season.Color + '20', color: season.Color, borderColor: season.Color }}>
{season.Name} {season.HijriYear}
</span>
</td>
<td>{season.StartDate}</td>
<td>{season.EndDate}</td>
<td>
<div className="season-actions">
<button className="btn-small" onClick={() => setEditing(true)}>Edit</button>
<button className="btn-small btn-danger" onClick={() => onDelete(season.Id!)}>Delete</button>
</div>
</td>
</tr>
);
}
return (
<tr className="editing">
<td>
<div className="season-edit-name">
<input type="text" value={form.Name} onChange={e => setForm({ ...form, Name: e.target.value })} placeholder="Name" />
<input type="number" value={form.HijriYear} onChange={e => setForm({ ...form, HijriYear: parseInt(e.target.value) || 0 })} placeholder="Year" style={{ width: 80 }} />
<input type="color" value={form.Color} onChange={e => setForm({ ...form, Color: e.target.value })} />
</div>
</td>
<td><input type="date" value={form.StartDate} onChange={e => setForm({ ...form, StartDate: e.target.value })} /></td>
<td><input type="date" value={form.EndDate} onChange={e => setForm({ ...form, EndDate: e.target.value })} /></td>
<td>
<div className="season-actions">
<button className="btn-small btn-primary" onClick={handleSave}>Save</button>
<button className="btn-small" onClick={() => setEditing(false)}>Cancel</button>
</div>
</td>
</tr>
);
}
interface SettingsProps {
onSeasonsChange: () => void;
}
function Settings({ onSeasonsChange }: SettingsProps) {
const { t } = useLanguage();
const [seasons, setSeasons] = useState<Season[]>([]);
const [loading, setLoading] = useState(true);
const [newSeason, setNewSeason] = useState<Omit<Season, 'Id'>>({
Name: '',
HijriYear: new Date().getFullYear() - 579, // rough Gregorian → Hijri
StartDate: '',
EndDate: '',
Color: DEFAULT_COLORS[0],
});
const loadSeasons = async () => {
setLoading(true);
const data = await fetchSeasons();
setSeasons(data);
setLoading(false);
};
useEffect(() => { loadSeasons(); }, []);
const handleCreate = async () => {
if (!newSeason.Name || !newSeason.StartDate || !newSeason.EndDate) return;
await createSeason(newSeason);
setNewSeason({
Name: '',
HijriYear: newSeason.HijriYear,
StartDate: '',
EndDate: '',
Color: DEFAULT_COLORS[(seasons.length + 1) % DEFAULT_COLORS.length],
});
await loadSeasons();
onSeasonsChange();
};
const handleSave = async (id: number, data: Partial<Season>) => {
await updateSeason(id, data);
await loadSeasons();
onSeasonsChange();
};
const handleDelete = async (id: number) => {
await deleteSeason(id);
await loadSeasons();
onSeasonsChange();
};
return (
<div className="settings-page">
<div className="page-title">
<h1>{t('settings.title')}</h1>
<p>{t('settings.subtitle')}</p>
</div>
<div className="chart-card">
<h2>{t('settings.seasons')}</h2>
<p className="settings-hint">{t('settings.seasonsHint')}</p>
<div className="table-container">
<table>
<thead>
<tr>
<th>{t('settings.seasonName')}</th>
<th>{t('settings.startDate')}</th>
<th>{t('settings.endDate')}</th>
<th>{t('settings.actions')}</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr><td colSpan={4} style={{ textAlign: 'center', padding: 24 }}>Loading...</td></tr>
) : (
seasons.map(s => (
<SeasonRow key={s.Id} season={s} onSave={handleSave} onDelete={handleDelete} />
))
)}
<tr className="add-row">
<td>
<div className="season-edit-name">
<input type="text" value={newSeason.Name} onChange={e => setNewSeason({ ...newSeason, Name: e.target.value })} placeholder={t('settings.namePlaceholder')} />
<input type="number" value={newSeason.HijriYear} onChange={e => setNewSeason({ ...newSeason, HijriYear: parseInt(e.target.value) || 0 })} style={{ width: 80 }} />
<input type="color" value={newSeason.Color} onChange={e => setNewSeason({ ...newSeason, Color: e.target.value })} />
</div>
</td>
<td><input type="date" value={newSeason.StartDate} onChange={e => setNewSeason({ ...newSeason, StartDate: e.target.value })} /></td>
<td><input type="date" value={newSeason.EndDate} onChange={e => setNewSeason({ ...newSeason, EndDate: e.target.value })} /></td>
<td>
<button className="btn-small btn-primary" onClick={handleCreate} disabled={!newSeason.Name || !newSeason.StartDate || !newSeason.EndDate}>
{t('settings.add')}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
);
}
export default Settings;