Bump version to v0.1.4.7
This commit is contained in:
@@ -68,6 +68,14 @@ interface News {
|
|||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PoliticalStatement {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
active?: boolean;
|
||||||
|
source?: string;
|
||||||
|
locale: string;
|
||||||
|
}
|
||||||
|
|
||||||
type SortField = 'id' | 'title' | 'artist' | 'createdAt' | 'releaseYear' | 'activations' | 'averageRating';
|
type SortField = 'id' | 'title' | 'artist' | 'createdAt' | 'releaseYear' | 'activations' | 'averageRating';
|
||||||
type SortDirection = 'asc' | 'desc';
|
type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
@@ -169,6 +177,11 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
|
|||||||
const [showSpecials, setShowSpecials] = useState(false);
|
const [showSpecials, setShowSpecials] = useState(false);
|
||||||
const [showGenres, setShowGenres] = useState(false);
|
const [showGenres, setShowGenres] = useState(false);
|
||||||
const [showNews, setShowNews] = useState(false);
|
const [showNews, setShowNews] = useState(false);
|
||||||
|
const [showPoliticalStatements, setShowPoliticalStatements] = useState(false);
|
||||||
|
const [politicalStatementsLocale, setPoliticalStatementsLocale] = useState<'de' | 'en'>('de');
|
||||||
|
const [politicalStatements, setPoliticalStatements] = useState<PoliticalStatement[]>([]);
|
||||||
|
const [newPoliticalStatementText, setNewPoliticalStatementText] = useState('');
|
||||||
|
const [newPoliticalStatementActive, setNewPoliticalStatementActive] = useState(true);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Check for existing auth on mount
|
// Check for existing auth on mount
|
||||||
@@ -447,6 +460,100 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Political Statements functions (JSON-backed via API)
|
||||||
|
const fetchPoliticalStatements = async (targetLocale: 'de' | 'en') => {
|
||||||
|
const res = await fetch(`/api/political-statements?locale=${encodeURIComponent(targetLocale)}&admin=true`, {
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
const enriched: PoliticalStatement[] = data.map((s: any) => ({
|
||||||
|
id: s.id,
|
||||||
|
text: s.text,
|
||||||
|
active: s.active !== false,
|
||||||
|
source: s.source,
|
||||||
|
locale: targetLocale,
|
||||||
|
}));
|
||||||
|
setPoliticalStatements(prev => {
|
||||||
|
const others = prev.filter(p => p.locale !== targetLocale);
|
||||||
|
return [...others, ...enriched];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreatePoliticalStatement = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newPoliticalStatementText.trim()) return;
|
||||||
|
|
||||||
|
const res = await fetch('/api/political-statements', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
locale: politicalStatementsLocale,
|
||||||
|
text: newPoliticalStatementText.trim(),
|
||||||
|
active: newPoliticalStatementActive,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setNewPoliticalStatementText('');
|
||||||
|
setNewPoliticalStatementActive(true);
|
||||||
|
fetchPoliticalStatements(politicalStatementsLocale);
|
||||||
|
} else {
|
||||||
|
alert('Failed to create statement');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditPoliticalStatementText = (locale: string, id: number, text: string) => {
|
||||||
|
setPoliticalStatements(prev =>
|
||||||
|
prev.map(s => (s.locale === locale && s.id === id ? { ...s, text } : s)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditPoliticalStatementActive = (locale: string, id: number, active: boolean) => {
|
||||||
|
setPoliticalStatements(prev =>
|
||||||
|
prev.map(s => (s.locale === locale && s.id === id ? { ...s, active } : s)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSavePoliticalStatement = async (locale: string, id: number) => {
|
||||||
|
const stmt = politicalStatements.find(s => s.locale === locale && s.id === id);
|
||||||
|
if (!stmt) return;
|
||||||
|
|
||||||
|
const res = await fetch('/api/political-statements', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
locale,
|
||||||
|
id,
|
||||||
|
text: stmt.text,
|
||||||
|
active: stmt.active !== false,
|
||||||
|
source: stmt.source,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
alert('Failed to save statement');
|
||||||
|
fetchPoliticalStatements(locale as 'de' | 'en');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletePoliticalStatement = async (locale: string, id: number) => {
|
||||||
|
if (!confirm('Delete this statement?')) return;
|
||||||
|
|
||||||
|
const res = await fetch('/api/political-statements', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({ locale, id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setPoliticalStatements(prev => prev.filter(s => !(s.locale === locale && s.id === id)));
|
||||||
|
} else {
|
||||||
|
alert('Failed to delete statement');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreateNews = async (e: React.FormEvent) => {
|
const handleCreateNews = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if ((!newNewsTitle.de.trim() && !newNewsTitle.en.trim()) || (!newNewsContent.de.trim() && !newNewsContent.en.trim())) return;
|
if ((!newNewsTitle.de.trim() && !newNewsTitle.en.trim()) || (!newNewsContent.de.trim() && !newNewsContent.en.trim())) return;
|
||||||
@@ -524,9 +631,13 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load specials after auth
|
// Load specials and political statements after auth
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) fetchSpecials();
|
if (isAuthenticated) {
|
||||||
|
fetchSpecials();
|
||||||
|
fetchPoliticalStatements('de');
|
||||||
|
fetchPoliticalStatements('en');
|
||||||
|
}
|
||||||
}, [isAuthenticated]);
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
const deleteGenre = async (id: number) => {
|
const deleteGenre = async (id: number) => {
|
||||||
@@ -1580,6 +1691,141 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Political Statements Management */}
|
||||||
|
<div className="admin-card" style={{ marginBottom: '2rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||||
|
<h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', margin: 0 }}>
|
||||||
|
Political Statements
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPoliticalStatements(!showPoliticalStatements)}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
background: '#f3f4f6',
|
||||||
|
border: '1px solid #d1d5db',
|
||||||
|
borderRadius: '0.25rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.875rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showPoliticalStatements ? t('hide') : t('show')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{showPoliticalStatements && (
|
||||||
|
<>
|
||||||
|
{/* Language Tabs */}
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
|
||||||
|
{(['de', 'en'] as const).map(lang => (
|
||||||
|
<button
|
||||||
|
key={lang}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPoliticalStatementsLocale(lang)}
|
||||||
|
style={{
|
||||||
|
padding: '0.4rem 0.8rem',
|
||||||
|
borderRadius: '999px',
|
||||||
|
border: '1px solid #d1d5db',
|
||||||
|
background: politicalStatementsLocale === lang ? '#111827' : 'white',
|
||||||
|
color: politicalStatementsLocale === lang ? 'white' : '#111827',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{lang.toUpperCase()}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create Form */}
|
||||||
|
<form onSubmit={handleCreatePoliticalStatement} style={{ marginBottom: '1rem' }}>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
|
<textarea
|
||||||
|
value={newPoliticalStatementText}
|
||||||
|
onChange={e => setNewPoliticalStatementText(e.target.value)}
|
||||||
|
placeholder="Statement text"
|
||||||
|
className="form-input"
|
||||||
|
rows={3}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', fontSize: '0.875rem' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={newPoliticalStatementActive}
|
||||||
|
onChange={e => setNewPoliticalStatementActive(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Active
|
||||||
|
</label>
|
||||||
|
<button type="submit" className="btn-primary" style={{ fontSize: '0.875rem' }}>
|
||||||
|
Add Statement ({politicalStatementsLocale.toUpperCase()})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* List */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
|
{politicalStatements
|
||||||
|
.filter(s => s.locale === politicalStatementsLocale)
|
||||||
|
.map(stmt => (
|
||||||
|
<div
|
||||||
|
key={`${stmt.locale}-${stmt.id}`}
|
||||||
|
style={{
|
||||||
|
background: stmt.active ? '#ecfdf5' : '#f3f4f6',
|
||||||
|
padding: '0.75rem',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.5rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
value={stmt.text}
|
||||||
|
onChange={e => handleEditPoliticalStatementText(stmt.locale, stmt.id, e.target.value)}
|
||||||
|
className="form-input"
|
||||||
|
rows={3}
|
||||||
|
style={{ fontSize: '0.85rem' }}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', fontSize: '0.8rem' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={stmt.active !== false}
|
||||||
|
onChange={e => handleEditPoliticalStatementActive(stmt.locale, stmt.id, e.target.checked)}
|
||||||
|
/>
|
||||||
|
Active
|
||||||
|
</label>
|
||||||
|
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-secondary"
|
||||||
|
style={{ padding: '0.25rem 0.6rem', fontSize: '0.75rem' }}
|
||||||
|
onClick={() => handleSavePoliticalStatement(stmt.locale, stmt.id)}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-danger"
|
||||||
|
style={{ padding: '0.25rem 0.6rem', fontSize: '0.75rem' }}
|
||||||
|
onClick={() => handleDeletePoliticalStatement(stmt.locale, stmt.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{politicalStatements.filter(s => s.locale === politicalStatementsLocale).length === 0 && (
|
||||||
|
<p style={{ color: '#666', fontSize: '0.875rem', textAlign: 'center', padding: '0.5rem' }}>
|
||||||
|
No statements for this language yet.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="admin-card" style={{ marginBottom: '2rem' }}>
|
<div className="admin-card" style={{ marginBottom: '2rem' }}>
|
||||||
<h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>{t('uploadSongs')}</h2>
|
<h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>{t('uploadSongs')}</h2>
|
||||||
<form onSubmit={handleBatchUpload}>
|
<form onSubmit={handleBatchUpload}>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { config } from "@/lib/config";
|
|||||||
import { generateBaseMetadata } from "@/lib/metadata";
|
import { generateBaseMetadata } from "@/lib/metadata";
|
||||||
import InstallPrompt from "@/components/InstallPrompt";
|
import InstallPrompt from "@/components/InstallPrompt";
|
||||||
import AppFooter from "@/components/AppFooter";
|
import AppFooter from "@/components/AppFooter";
|
||||||
|
import PoliticalStatementBanner from "@/components/PoliticalStatementBanner";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -89,6 +90,7 @@ export default async function LocaleLayout({
|
|||||||
{children}
|
{children}
|
||||||
<InstallPrompt />
|
<InstallPrompt />
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
|
<PoliticalStatementBanner />
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoerdle",
|
"name": "hoerdle",
|
||||||
"version": "0.1.4.6",
|
"version": "0.1.4.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user