331 lines
9.0 KiB
TypeScript
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;
|