fix: restore deleted curator implementation files
This commit is contained in:
2164
app/curator/CuratorPageClient.tsx
Normal file
2164
app/curator/CuratorPageClient.tsx
Normal file
File diff suppressed because it is too large
Load Diff
171
app/curator/help/CuratorHelpClient.tsx
Normal file
171
app/curator/help/CuratorHelpClient.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTranslations, useLocale } from 'next-intl';
|
||||||
|
import { Link } from '@/lib/navigation';
|
||||||
|
|
||||||
|
export default function CuratorHelpClient() {
|
||||||
|
const t = useTranslations('CuratorHelp');
|
||||||
|
const locale = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main style={{ maxWidth: '960px', margin: '2rem auto', padding: '1rem' }}>
|
||||||
|
<header style={{ marginBottom: '2rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<h1 style={{ fontSize: '1.75rem', marginBottom: '0.25rem' }}>{t('title')}</h1>
|
||||||
|
<Link
|
||||||
|
href="/curator"
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
background: '#6b7280',
|
||||||
|
color: 'white',
|
||||||
|
textDecoration: 'none',
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('backToDashboard')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||||
|
{/* Einführung */}
|
||||||
|
<section>
|
||||||
|
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem', borderBottom: '2px solid #e5e7eb', paddingBottom: '0.5rem' }}>
|
||||||
|
{t('introductionTitle')}
|
||||||
|
</h2>
|
||||||
|
<div style={{ fontSize: '0.95rem', lineHeight: '1.7' }}>
|
||||||
|
<p style={{ marginBottom: '1rem' }}>{t('introductionText')}</p>
|
||||||
|
<h3 style={{ fontSize: '1.1rem', marginTop: '1.5rem', marginBottom: '0.75rem' }}>{t('permissionsTitle')}</h3>
|
||||||
|
<ul style={{ marginLeft: '1.5rem', marginBottom: '1rem' }}>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('permission1')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('permission2')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('permission3')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('permission4')}</li>
|
||||||
|
</ul>
|
||||||
|
<p style={{ marginTop: '1rem', padding: '0.75rem', background: '#fef3c7', borderRadius: '0.375rem', border: '1px solid #fbbf24' }}>
|
||||||
|
<strong>{t('note')}:</strong> {t('permissionNote')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Song-Upload */}
|
||||||
|
<section>
|
||||||
|
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem', borderBottom: '2px solid #e5e7eb', paddingBottom: '0.5rem' }}>
|
||||||
|
{t('uploadTitle')}
|
||||||
|
</h2>
|
||||||
|
<div style={{ fontSize: '0.95rem', lineHeight: '1.7' }}>
|
||||||
|
<h3 style={{ fontSize: '1.1rem', marginTop: '1rem', marginBottom: '0.75rem' }}>{t('uploadStepsTitle')}</h3>
|
||||||
|
<ol style={{ marginLeft: '1.5rem', marginBottom: '1rem' }}>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('uploadStep1')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('uploadStep2')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('uploadStep3')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('uploadStep4')}</li>
|
||||||
|
</ol>
|
||||||
|
<h3 style={{ fontSize: '1.1rem', marginTop: '1.5rem', marginBottom: '0.75rem' }}>{t('uploadBestPracticesTitle')}</h3>
|
||||||
|
<ul style={{ marginLeft: '1.5rem', marginBottom: '1rem' }}>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('uploadBestPractice1')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('uploadBestPractice2')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('uploadBestPractice3')}</li>
|
||||||
|
</ul>
|
||||||
|
<p style={{ marginTop: '1rem', padding: '0.75rem', background: '#dbeafe', borderRadius: '0.375rem', border: '1px solid #3b82f6' }}>
|
||||||
|
<strong>{t('tip')}:</strong> {t('uploadTip')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Song-Bearbeitung */}
|
||||||
|
<section>
|
||||||
|
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem', borderBottom: '2px solid #e5e7eb', paddingBottom: '0.5rem' }}>
|
||||||
|
{t('editingTitle')}
|
||||||
|
</h2>
|
||||||
|
<div style={{ fontSize: '0.95rem', lineHeight: '1.7' }}>
|
||||||
|
<h3 style={{ fontSize: '1.1rem', marginTop: '1rem', marginBottom: '0.75rem' }}>{t('singleEditTitle')}</h3>
|
||||||
|
<p style={{ marginBottom: '1rem' }}>{t('singleEditText')}</p>
|
||||||
|
<h3 style={{ fontSize: '1.1rem', marginTop: '1.5rem', marginBottom: '0.75rem' }}>{t('batchEditTitle')}</h3>
|
||||||
|
<p style={{ marginBottom: '1rem' }}>{t('batchEditText')}</p>
|
||||||
|
<ul style={{ marginLeft: '1.5rem', marginBottom: '1rem' }}>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('batchEditFeature1')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('batchEditFeature2')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('batchEditFeature3')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('batchEditFeature4')}</li>
|
||||||
|
</ul>
|
||||||
|
<h3 style={{ fontSize: '1.1rem', marginTop: '1.5rem', marginBottom: '0.75rem' }}>{t('genreSpecialAssignmentTitle')}</h3>
|
||||||
|
<p style={{ marginBottom: '1rem' }}>{t('genreSpecialAssignmentText')}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Specials kuratieren */}
|
||||||
|
<section>
|
||||||
|
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem', borderBottom: '2px solid #e5e7eb', paddingBottom: '0.5rem' }}>
|
||||||
|
{t('curateSpecialsHelpTitle')}
|
||||||
|
</h2>
|
||||||
|
<div style={{ fontSize: '0.95rem', lineHeight: '1.7' }}>
|
||||||
|
<p style={{ marginBottom: '1rem' }}>{t('curateSpecialsHelpIntro')}</p>
|
||||||
|
<h3 style={{ fontSize: '1.1rem', marginTop: '1rem', marginBottom: '0.75rem' }}>
|
||||||
|
{t('curateSpecialsHelpStepsTitle')}
|
||||||
|
</h3>
|
||||||
|
<ol style={{ marginLeft: '1.5rem', marginBottom: '1rem' }}>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('curateSpecialsHelpStep1')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('curateSpecialsHelpStep2')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('curateSpecialsHelpStep3')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}>{t('curateSpecialsHelpStep4')}</li>
|
||||||
|
</ol>
|
||||||
|
<p style={{ marginTop: '1rem', padding: '0.75rem', background: '#fef3c7', borderRadius: '0.375rem', border: '1px solid #fbbf24' }}>
|
||||||
|
<strong>{t('note')}:</strong> {t('curateSpecialsPermissionsNote')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Kommentar-Verwaltung */}
|
||||||
|
<section>
|
||||||
|
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem', borderBottom: '2px solid #e5e7eb', paddingBottom: '0.5rem' }}>
|
||||||
|
{t('commentsTitle')}
|
||||||
|
</h2>
|
||||||
|
<div style={{ fontSize: '0.95rem', lineHeight: '1.7' }}>
|
||||||
|
<p style={{ marginBottom: '1rem' }}>{t('commentsText')}</p>
|
||||||
|
<h3 style={{ fontSize: '1.1rem', marginTop: '1rem', marginBottom: '0.75rem' }}>{t('commentsActionsTitle')}</h3>
|
||||||
|
<ul style={{ marginLeft: '1.5rem', marginBottom: '1rem' }}>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}><strong>{t('markAsRead')}:</strong> {t('markAsReadText')}</li>
|
||||||
|
<li style={{ marginBottom: '0.5rem' }}><strong>{t('archive')}:</strong> {t('archiveText')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Best Practices */}
|
||||||
|
<section>
|
||||||
|
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem', borderBottom: '2px solid #e5e7eb', paddingBottom: '0.5rem' }}>
|
||||||
|
{t('bestPracticesTitle')}
|
||||||
|
</h2>
|
||||||
|
<div style={{ fontSize: '0.95rem', lineHeight: '1.7' }}>
|
||||||
|
<ul style={{ marginLeft: '1.5rem', marginBottom: '1rem' }}>
|
||||||
|
<li style={{ marginBottom: '0.75rem' }}>{t('bestPractice1')}</li>
|
||||||
|
<li style={{ marginBottom: '0.75rem' }}>{t('bestPractice2')}</li>
|
||||||
|
<li style={{ marginBottom: '0.75rem' }}>{t('bestPractice3')}</li>
|
||||||
|
<li style={{ marginBottom: '0.75rem' }}>{t('bestPractice4')}</li>
|
||||||
|
<li style={{ marginBottom: '0.75rem' }}>{t('bestPractice5')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Troubleshooting */}
|
||||||
|
<section>
|
||||||
|
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem', borderBottom: '2px solid #e5e7eb', paddingBottom: '0.5rem' }}>
|
||||||
|
{t('troubleshootingTitle')}
|
||||||
|
</h2>
|
||||||
|
<div style={{ fontSize: '0.95rem', lineHeight: '1.7' }}>
|
||||||
|
<h3 style={{ fontSize: '1.1rem', marginTop: '1rem', marginBottom: '0.75rem' }}>{t('troubleshootingQ1')}</h3>
|
||||||
|
<p style={{ marginBottom: '1rem', marginLeft: '1rem' }}>{t('troubleshootingA1')}</p>
|
||||||
|
<h3 style={{ fontSize: '1.1rem', marginTop: '1rem', marginBottom: '0.75rem' }}>{t('troubleshootingQ2')}</h3>
|
||||||
|
<p style={{ marginBottom: '1rem', marginLeft: '1rem' }}>{t('troubleshootingA2')}</p>
|
||||||
|
<h3 style={{ fontSize: '1.1rem', marginTop: '1rem', marginBottom: '0.75rem' }}>{t('troubleshootingQ3')}</h3>
|
||||||
|
<p style={{ marginBottom: '1rem', marginLeft: '1rem' }}>{t('troubleshootingA3')}</p>
|
||||||
|
<h3 style={{ fontSize: '1.1rem', marginTop: '1rem', marginBottom: '0.75rem' }}>{t('troubleshootingQ4')}</h3>
|
||||||
|
<p style={{ marginBottom: '1rem', marginLeft: '1rem' }}>{t('troubleshootingA4')}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
8
app/curator/help/page.tsx
Normal file
8
app/curator/help/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
import CuratorHelpClient from './CuratorHelpClient';
|
||||||
|
|
||||||
|
export default function CuratorHelpPage() {
|
||||||
|
return <CuratorHelpClient />;
|
||||||
|
}
|
||||||
|
|
||||||
11
app/curator/page.tsx
Normal file
11
app/curator/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Server-Wrapper für die Kuratoren-Seite.
|
||||||
|
// Markiert die Route als dynamisch und rendert die eigentliche Client-Komponente.
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
import CuratorPageClient from './CuratorPageClient';
|
||||||
|
|
||||||
|
export default function CuratorPage() {
|
||||||
|
return <CuratorPageClient />;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
156
app/curator/specials/CuratorSpecialsClient.tsx
Normal file
156
app/curator/specials/CuratorSpecialsClient.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
|
import { Link } from '@/lib/navigation';
|
||||||
|
import { getCuratorAuthHeaders } from '@/lib/curatorAuth';
|
||||||
|
import { getLocalizedValue } from '@/lib/i18n';
|
||||||
|
|
||||||
|
interface CuratorSpecial {
|
||||||
|
id: number;
|
||||||
|
name: string | { de?: string; en?: string };
|
||||||
|
songCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CuratorSpecialsClient() {
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const urlLocale = pathname?.split('/')[1] as 'de' | 'en' | undefined;
|
||||||
|
const intlLocale = useLocale() as 'de' | 'en';
|
||||||
|
const locale: 'de' | 'en' = urlLocale === 'de' || urlLocale === 'en' ? urlLocale : intlLocale;
|
||||||
|
const t = useTranslations('Curator');
|
||||||
|
|
||||||
|
const [specials, setSpecials] = useState<CuratorSpecial[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSpecials = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await fetch('/api/curator/specials', {
|
||||||
|
headers: getCuratorAuthHeaders(),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 403) {
|
||||||
|
setError(t('specialForbidden'));
|
||||||
|
} else {
|
||||||
|
setError('Failed to load specials');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
setSpecials(data);
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load specials');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSpecials();
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: '960px', margin: '2rem auto', padding: '1rem' }}>
|
||||||
|
<p>{t('loading')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: '960px', margin: '2rem auto', padding: '1rem' }}>
|
||||||
|
<p style={{ color: 'red' }}>{error}</p>
|
||||||
|
<Link
|
||||||
|
href="/curator"
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
marginTop: '1rem',
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
background: '#6b7280',
|
||||||
|
color: 'white',
|
||||||
|
textDecoration: 'none',
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('backToDashboard') || 'Back to Dashboard'}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: '960px', margin: '2rem auto', padding: '1rem' }}>
|
||||||
|
<header style={{ marginBottom: '2rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<h1 style={{ fontSize: '1.75rem', marginBottom: '0.25rem' }}>
|
||||||
|
{t('curateSpecialsTitle') || 'Curate Specials'}
|
||||||
|
</h1>
|
||||||
|
<Link
|
||||||
|
href="/curator"
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
background: '#6b7280',
|
||||||
|
color: 'white',
|
||||||
|
textDecoration: 'none',
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('backToDashboard') || 'Back to Dashboard'}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{specials.length === 0 ? (
|
||||||
|
<div style={{ padding: '2rem', textAlign: 'center', color: '#666' }}>
|
||||||
|
<p>{t('noSpecialsAssigned') || 'No specials assigned to you.'}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
{specials.map((special) => (
|
||||||
|
<Link
|
||||||
|
key={special.id}
|
||||||
|
href={`/curator/specials/${special.id}`}
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
padding: '1.5rem',
|
||||||
|
background: '#f9fafb',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#f3f4f6';
|
||||||
|
e.currentTarget.style.borderColor = '#d1d5db';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = '#f9fafb';
|
||||||
|
e.currentTarget.style.borderColor = '#e5e7eb';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<div>
|
||||||
|
<h2 style={{ fontSize: '1.25rem', marginBottom: '0.5rem', color: '#111827' }}>
|
||||||
|
{getLocalizedValue(special.name, locale)}
|
||||||
|
</h2>
|
||||||
|
<p style={{ fontSize: '0.875rem', color: '#6b7280' }}>
|
||||||
|
{special.songCount} {special.songCount === 1 ? 'song' : 'songs'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '1.5rem', color: '#10b981' }}>→</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
177
app/curator/specials/[id]/page.tsx
Normal file
177
app/curator/specials/[id]/page.tsx
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useParams, useRouter, usePathname } from 'next/navigation';
|
||||||
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
|
import CurateSpecialEditor, { CurateSpecial } from '@/components/CurateSpecialEditor';
|
||||||
|
import { getCuratorAuthHeaders } from '@/lib/curatorAuth';
|
||||||
|
import HelpTooltip from '@/components/HelpTooltip';
|
||||||
|
|
||||||
|
export default function CuratorSpecialEditorPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const urlLocale = pathname?.split('/')[1] as 'de' | 'en' | undefined;
|
||||||
|
const intlLocale = useLocale() as 'de' | 'en';
|
||||||
|
const locale: 'de' | 'en' = urlLocale === 'de' || urlLocale === 'en' ? urlLocale : intlLocale;
|
||||||
|
const t = useTranslations('Curator');
|
||||||
|
const tHelp = useTranslations('CuratorHelp');
|
||||||
|
|
||||||
|
const specialId = params?.id as string;
|
||||||
|
|
||||||
|
const [special, setSpecial] = useState<CurateSpecial | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchSpecial = async (showLoading = true) => {
|
||||||
|
try {
|
||||||
|
if (showLoading) {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
const res = await fetch(`/api/curator/specials/${specialId}`, {
|
||||||
|
headers: getCuratorAuthHeaders(),
|
||||||
|
});
|
||||||
|
if (res.status === 403) {
|
||||||
|
setError(t('specialForbidden'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
setError('Failed to load special');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
setSpecial(data);
|
||||||
|
} catch (e) {
|
||||||
|
setError('Failed to load special');
|
||||||
|
} finally {
|
||||||
|
if (showLoading) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (specialId) {
|
||||||
|
fetchSpecial(true);
|
||||||
|
}
|
||||||
|
}, [specialId, t]);
|
||||||
|
|
||||||
|
const handleSaveStartTime = async (songId: number, startTime: number) => {
|
||||||
|
const res = await fetch(`/api/curator/specials/${specialId}/songs`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
...getCuratorAuthHeaders(),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ songId, startTime }),
|
||||||
|
});
|
||||||
|
if (res.status === 403) {
|
||||||
|
setError(t('specialForbidden'));
|
||||||
|
} else if (!res.ok) {
|
||||||
|
setError('Failed to save changes');
|
||||||
|
} else {
|
||||||
|
// Reload special data to update the start time in the song list
|
||||||
|
await fetchSpecial(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||||
|
<p>{t('loadingData')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||||
|
<p>{error}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push(`/${locale}/curator`)}
|
||||||
|
style={{
|
||||||
|
marginTop: '1rem',
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
border: 'none',
|
||||||
|
background: '#e5e7eb',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('backToDashboard')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!special) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||||
|
<p>{t('specialNotFound')}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push(`/${locale}/curator`)}
|
||||||
|
style={{
|
||||||
|
marginTop: '1rem',
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
border: 'none',
|
||||||
|
background: '#e5e7eb',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('backToDashboard')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<h1 style={{ fontSize: '1.75rem', fontWeight: 'bold' }}>
|
||||||
|
{t('curateSpecialHeaderPrefix')}
|
||||||
|
</h1>
|
||||||
|
<HelpTooltip
|
||||||
|
shortText={tHelp('tooltipCurateSpecialEditorShort')}
|
||||||
|
longText={tHelp('tooltipCurateSpecialEditorLong')}
|
||||||
|
position="bottom"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.push(`/${locale}/curator/specials`)}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
background: '#e5e7eb',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('backToCuratorSpecials')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CurateSpecialEditor
|
||||||
|
special={special}
|
||||||
|
locale={locale}
|
||||||
|
onBack={() => router.push(`/${locale}/curator/specials`)}
|
||||||
|
onSaveStartTime={handleSaveStartTime}
|
||||||
|
backLabel={t('backToCuratorSpecials')}
|
||||||
|
headerPrefix={t('curateSpecialHeaderPrefix')}
|
||||||
|
noSongsHint={t('curateSpecialNoSongs')}
|
||||||
|
noSongsSubHint={t('curateSpecialNoSongsSub')}
|
||||||
|
instructionsText={t('curateSpecialInstructions')}
|
||||||
|
savingLabel={t('saving')}
|
||||||
|
saveChangesLabel={t('saveChanges')}
|
||||||
|
savedLabel={t('saved')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
13
app/curator/specials/page.tsx
Normal file
13
app/curator/specials/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
// Root /curator/specials route without locale:
|
||||||
|
// redirect users to the default English locale version.
|
||||||
|
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
export default function CuratorSpecialsPage() {
|
||||||
|
redirect('/en/curator/specials');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user