feat: landing page, schedule action, editorial calendar LLM, Fase D foundations
Landing Page: - Public landing page at /landing with hero, features grid, CTA - ProtectedRoute redirects to /landing instead of /login when not auth'd - Editorial Fresh design: Fraunces headings, clamp() responsive sizing Schedule Action: - "Schedula" button appears after approving a post - ScheduleModal: date/time picker, creates ScheduledPost via API - Reminder to connect social accounts for automatic publishing Editorial Calendar LLM: - Backend: generate-calendar now calls LLM to generate hook + brief for each slot - Uses character profile (voice, target, niche) for contextual ideas - Respects brief strategico from the UI - Frontend: slots show AI-generated hook (Fraunces serif) + brief description - Each slot has "Genera contenuto →" link for one-click content generation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import { api } from '../api'
|
||||
import { useAuth } from '../AuthContext'
|
||||
@@ -50,6 +51,9 @@ export default function ContentPage() {
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [editText, setEditText] = useState('')
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const [showScheduleModal, setShowScheduleModal] = useState(false)
|
||||
const [scheduleDate, setScheduleDate] = useState('')
|
||||
const [scheduleTime, setScheduleTime] = useState('09:00')
|
||||
|
||||
const [form, setForm] = useState({
|
||||
character_id: '',
|
||||
@@ -172,6 +176,21 @@ export default function ContentPage() {
|
||||
} catch (err) { setError(err.message || 'Errore eliminazione') }
|
||||
}
|
||||
|
||||
const handleSchedule = async () => {
|
||||
if (!generated || !scheduleDate) return
|
||||
try {
|
||||
const scheduledAt = new Date(`${scheduleDate}T${scheduleTime}:00`).toISOString()
|
||||
await api.post('/plans/schedule', {
|
||||
post_id: generated.id,
|
||||
platform: generated.platform_hint || 'instagram',
|
||||
scheduled_at: scheduledAt,
|
||||
})
|
||||
updateActivePost({ status: 'scheduled' })
|
||||
setShowScheduleModal(false)
|
||||
setError('')
|
||||
} catch (err) { setError(err.message || 'Errore nella schedulazione') }
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ animation: 'fade-up 0.5s ease-out both' }}>
|
||||
{/* Header */}
|
||||
@@ -429,13 +448,21 @@ export default function ContentPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.5rem', paddingTop: '1rem', borderTop: '1px solid var(--border)', flexWrap: 'wrap' }}>
|
||||
{generated.status !== 'approved' && (
|
||||
<div style={{ display: 'flex', gap: '0.5rem', paddingTop: '1rem', borderTop: '1px solid var(--border)', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
{generated.status !== 'approved' && generated.status !== 'scheduled' && (
|
||||
<button onClick={handleApprove} style={{ ...btnPrimary, backgroundColor: 'var(--success)' }}>Approva</button>
|
||||
)}
|
||||
{generated.status === 'approved' && (
|
||||
<button onClick={() => setShowScheduleModal(true)} style={{ ...btnPrimary, backgroundColor: '#3B82F6' }}>Schedula</button>
|
||||
)}
|
||||
{!editing && <button onClick={() => setEditing(true)} style={btnSecondary}>Modifica</button>}
|
||||
<button onClick={() => setShowDeleteConfirm(true)} style={{ ...btnSecondary, marginLeft: 'auto', color: 'var(--error)' }}>Elimina</button>
|
||||
</div>
|
||||
{generated.status === 'approved' && (
|
||||
<p style={{ fontSize: '0.75rem', color: 'var(--ink-muted)', marginTop: '0.5rem', lineHeight: 1.5 }}>
|
||||
Per pubblicare automaticamente, connetti i tuoi account social in <Link to="/social" style={{ color: 'var(--accent)' }}>Impostazioni Social</Link>.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '4rem 1rem', textAlign: 'center' }}>
|
||||
@@ -458,11 +485,69 @@ export default function ContentPage() {
|
||||
onConfirm={handleDelete}
|
||||
onCancel={() => setShowDeleteConfirm(false)}
|
||||
/>
|
||||
{showScheduleModal && (
|
||||
<ScheduleModal
|
||||
date={scheduleDate}
|
||||
time={scheduleTime}
|
||||
onDateChange={setScheduleDate}
|
||||
onTimeChange={setScheduleTime}
|
||||
onConfirm={handleSchedule}
|
||||
onCancel={() => setShowScheduleModal(false)}
|
||||
platform={generated?.platform_hint}
|
||||
/>
|
||||
)}
|
||||
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ScheduleModal({ date, time, onDateChange, onTimeChange, onConfirm, onCancel, platform }) {
|
||||
// Default to tomorrow
|
||||
const tomorrow = new Date(Date.now() + 86400000).toISOString().split('T')[0]
|
||||
if (!date) onDateChange(tomorrow)
|
||||
|
||||
return createPortal(
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 99999, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div onClick={onCancel} style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(26,26,26,0.5)', backdropFilter: 'blur(3px)' }} />
|
||||
<div style={{
|
||||
position: 'relative', backgroundColor: 'var(--surface)', border: '1px solid var(--border)',
|
||||
borderTop: '4px solid #3B82F6', padding: '2rem', maxWidth: 400, width: '90%',
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.15)',
|
||||
}}>
|
||||
<h3 style={{ fontFamily: "'Fraunces', serif", fontSize: '1.15rem', fontWeight: 600, color: 'var(--ink)', margin: '0 0 0.5rem' }}>
|
||||
Schedula pubblicazione
|
||||
</h3>
|
||||
<p style={{ fontSize: '0.82rem', color: 'var(--ink-muted)', margin: '0 0 1.25rem', lineHeight: 1.5 }}>
|
||||
Scegli data e ora per la pubblicazione{platform ? ` su ${platform}` : ''}.
|
||||
Il post verrà pubblicato automaticamente quando i social saranno connessi.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', marginBottom: '1.5rem' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ display: 'block', fontSize: '0.7rem', fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--ink-muted)', marginBottom: '0.3rem' }}>Data</label>
|
||||
<input type="date" value={date || tomorrow} onChange={e => onDateChange(e.target.value)}
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
style={{ width: '100%', padding: '0.5rem', border: '1px solid var(--border)', fontSize: '0.875rem', fontFamily: "'DM Sans', sans-serif", outline: 'none' }} />
|
||||
</div>
|
||||
<div style={{ width: 100 }}>
|
||||
<label style={{ display: 'block', fontSize: '0.7rem', fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--ink-muted)', marginBottom: '0.3rem' }}>Ora</label>
|
||||
<input type="time" value={time} onChange={e => onTimeChange(e.target.value)}
|
||||
style={{ width: '100%', padding: '0.5rem', border: '1px solid var(--border)', fontSize: '0.875rem', fontFamily: "'DM Sans', sans-serif", outline: 'none' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
|
||||
<button onClick={onCancel} style={{ padding: '0.5rem 1rem', fontSize: '0.85rem', fontWeight: 600, fontFamily: "'DM Sans', sans-serif", backgroundColor: 'var(--cream-dark)', color: 'var(--ink-light)', border: 'none', cursor: 'pointer' }}>
|
||||
Annulla
|
||||
</button>
|
||||
<button onClick={onConfirm} style={{ padding: '0.5rem 1rem', fontSize: '0.85rem', fontWeight: 600, fontFamily: "'DM Sans', sans-serif", backgroundColor: '#3B82F6', color: 'white', border: 'none', cursor: 'pointer' }}>
|
||||
Schedula
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
|
||||
function HashtagEditor({ hashtags, onChange, postId }) {
|
||||
const [newTag, setNewTag] = useState('')
|
||||
const [editIdx, setEditIdx] = useState(null)
|
||||
|
||||
Reference in New Issue
Block a user