Files
appcakes-builds/src/pages/AdminNewsForm.tsx
T
2026-07-13 08:54:39 +00:00

331 lines
9.0 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
IonPage,
IonContent,
IonHeader,
IonToolbar,
IonTitle,
IonButtons,
IonButton,
IonIcon,
IonSpinner,
IonItem,
IonLabel,
IonInput,
IonTextarea,
} from '@ionic/react';
import { useHistory, useParams } from 'react-router-dom';
import { supabase } from '../supabase';
import { useAuth } from '../contexts/AuthContext';
import { chevronBackOutline, saveOutline, trashOutline } from 'ionicons/icons';
const AdminNewsForm: React.FC = () => {
const history = useHistory();
const { id } = useParams<{ id: string }>();
const isEdit = !!id && id !== 'new';
const { user, profile } = useAuth();
const [loading, setLoading] = useState(false);
const [initialLoading, setInitialLoading] = useState(isEdit);
const [error, setError] = useState<string | null>(null);
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [imageUrl, setImageUrl] = useState('');
const showError = (msg: string) => {
setError(msg);
setTimeout(() => setError(null), 4000);
};
useEffect(() => {
if (profile?.role !== 'admin') {
history.replace('/profile');
return;
}
if (isEdit) {
loadNewsItem();
}
}, [profile, isEdit]);
const loadNewsItem = async () => {
try {
const { data, error } = await supabase
.from('news')
.select('*')
.eq('id', id)
.single();
if (error) throw error;
if (data) {
setTitle(data.title);
setContent(data.content);
setImageUrl(data.image_url || '');
}
} catch (err: any) {
showError(err.message);
} finally {
setInitialLoading(false);
}
};
const handleSave = async () => {
if (!title.trim() || !content.trim()) {
showError('Title and content are required.');
return;
}
setLoading(true);
try {
if (isEdit) {
const { error } = await supabase
.from('news')
.update({
title,
content,
image_url: imageUrl.trim() || null,
})
.eq('id', id);
if (error) throw error;
} else {
const { error } = await supabase.from('news').insert({
title,
content,
image_url: imageUrl.trim() || null,
created_by: user!.id,
});
if (error) throw error;
}
history.goBack();
} catch (err: any) {
showError(err.message);
setLoading(false);
}
};
const handleDelete = async () => {
if (!window.confirm('Delete this news item?')) return;
setLoading(true);
try {
const { error } = await supabase.from('news').delete().eq('id', id);
if (error) throw error;
history.goBack();
} catch (err: any) {
showError(err.message);
setLoading(false);
}
};
if (initialLoading) {
return (
<IonPage>
<IonHeader className="ion-no-border">
<IonToolbar style={{ '--background': '#f5f7f4' }}>
<IonButtons slot="start">
<IonButton onClick={() => history.goBack()}>
<IonIcon icon={chevronBackOutline} slot="icon-only" />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent style={{ '--background': '#f5f7f4' }}>
<div
style={{
display: 'flex',
justifyContent: 'center',
marginTop: '40px',
}}
>
<IonSpinner name="crescent" />
</div>
</IonContent>
</IonPage>
);
}
return (
<IonPage>
<IonHeader className="ion-no-border">
<IonToolbar style={{ '--background': '#f5f7f4' }}>
<IonButtons slot="start">
<IonButton onClick={() => history.goBack()}>
<IonIcon icon={chevronBackOutline} slot="icon-only" />
</IonButton>
</IonButtons>
<IonTitle style={{ fontWeight: '700', fontSize: '18px' }}>
{isEdit ? 'Edit News' : 'Create News'}
</IonTitle>
{isEdit && (
<IonButtons slot="end">
<IonButton onClick={handleDelete} color="danger">
<IonIcon icon={trashOutline} slot="icon-only" />
</IonButton>
</IonButtons>
)}
</IonToolbar>
</IonHeader>
<IonContent style={{ '--background': '#f5f7f4' }}>
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
{error && (
<div
style={{
background: '#fee2e2',
color: '#991b1b',
padding: '12px',
borderRadius: '8px',
fontSize: '14px',
marginBottom: '20px',
}}
>
{error}
</div>
)}
<div
style={{
background: '#ffffff',
borderRadius: '24px',
padding: '20px',
display: 'flex',
flexDirection: 'column',
gap: '16px',
boxShadow: '0 2px 4px rgba(0,0,0,0.02)',
}}
>
<div>
<label
style={{
display: 'block',
fontSize: '12px',
fontWeight: '600',
color: '#4b5563',
marginBottom: '8px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
>
Title
</label>
<IonItem
lines="none"
style={{
'--background': '#f9faf9',
'--border-radius': '12px',
'--padding-start': '16px',
}}
>
<IonInput
value={title}
onIonChange={(e) => setTitle(e.detail.value!)}
placeholder="Enter news title"
style={{ fontWeight: '500' }}
/>
</IonItem>
</div>
<div>
<label
style={{
display: 'block',
fontSize: '12px',
fontWeight: '600',
color: '#4b5563',
marginBottom: '8px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
>
Image URL (Optional)
</label>
<IonItem
lines="none"
style={{
'--background': '#f9faf9',
'--border-radius': '12px',
'--padding-start': '16px',
}}
>
<IonInput
value={imageUrl}
onIonChange={(e) => setImageUrl(e.detail.value!)}
placeholder="https://example.com/image.jpg"
/>
</IonItem>
</div>
<div>
<label
style={{
display: 'block',
fontSize: '12px',
fontWeight: '600',
color: '#4b5563',
marginBottom: '8px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
>
Content
</label>
<IonItem
lines="none"
style={{
'--background': '#f9faf9',
'--border-radius': '12px',
'--padding-start': '16px',
alignItems: 'flex-start',
}}
>
<IonTextarea
value={content}
onIonChange={(e) => setContent(e.detail.value!)}
placeholder="Write your news update here..."
autoGrow
style={{ minHeight: '150px' }}
/>
</IonItem>
</div>
</div>
<button
onClick={handleSave}
disabled={loading}
style={{
width: '100%',
background: '#2f6b4f',
color: '#ffffff',
border: 'none',
borderRadius: '999px',
padding: '16px',
fontSize: '16px',
fontWeight: '600',
marginTop: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.7 : 1,
}}
>
{loading ? (
<IonSpinner
name="crescent"
style={{ width: '24px', height: '24px' }}
/>
) : (
<>
<IonIcon icon={saveOutline} />
Save News
</>
)}
</button>
</div>
</IonContent>
</IonPage>
);
};
export default AdminNewsForm;