+ {shows.length > 0 && (
+
+
+
+
+ )}
+
+
+
setPublishedAt(e.target.value)}
+ min={new Date().toISOString().slice(0, 16)}
+ className="rounded-2xl border-2 border-border bg-surface-warm/50 px-4 py-3 text-sm text-text outline-none transition-all duration-200 focus:border-primary focus:bg-surface focus:shadow-[0_0_0_4px_rgba(91,76,219,0.08)]"
+ />
+ {publishedAt && (
+
+ Sera publié le {new Date(publishedAt).toLocaleString('fr-FR')}
+
+ )}
+
+
+ )}
+
{/* Chapter editor — original mode only */}
{mode === 'original' && audioFile && (
diff --git a/src/stores/notifications.ts b/src/stores/notifications.ts
new file mode 100644
index 0000000..667e93d
--- /dev/null
+++ b/src/stores/notifications.ts
@@ -0,0 +1,47 @@
+import { create } from 'zustand'
+import { supabase } from '@/lib/supabase'
+import type { Notification } from '@/types'
+
+interface NotificationsStore {
+ notifications: Notification[]
+ unreadCount: number
+ fetch: (userId: string) => Promise
+ markRead: (id: string) => Promise
+ markAllRead: (userId: string) => Promise
+}
+
+export const useNotificationsStore = create((set) => ({
+ notifications: [],
+ unreadCount: 0,
+
+ fetch: async (userId) => {
+ const { data } = await supabase
+ .from('notifications')
+ .select('*')
+ .eq('user_id', userId)
+ .order('created_at', { ascending: false })
+ .limit(50)
+ if (data) {
+ set({
+ notifications: data,
+ unreadCount: data.filter((n) => !n.read).length,
+ })
+ }
+ },
+
+ markRead: async (id) => {
+ await supabase.from('notifications').update({ read: true }).eq('id', id)
+ set((s) => ({
+ notifications: s.notifications.map((n) => n.id === id ? { ...n, read: true } : n),
+ unreadCount: Math.max(0, s.unreadCount - 1),
+ }))
+ },
+
+ markAllRead: async (userId) => {
+ await supabase.from('notifications').update({ read: true }).eq('user_id', userId).eq('read', false)
+ set((s) => ({
+ notifications: s.notifications.map((n) => ({ ...n, read: true })),
+ unreadCount: 0,
+ }))
+ },
+}))
diff --git a/src/stores/playlist-modal.ts b/src/stores/playlist-modal.ts
new file mode 100644
index 0000000..f239176
--- /dev/null
+++ b/src/stores/playlist-modal.ts
@@ -0,0 +1,13 @@
+import { create } from 'zustand'
+
+interface PlaylistModalStore {
+ podcastId: string | null
+ open: (id: string) => void
+ close: () => void
+}
+
+export const usePlaylistModal = create((set) => ({
+ podcastId: null,
+ open: (id) => set({ podcastId: id }),
+ close: () => set({ podcastId: null }),
+}))
diff --git a/src/types/index.ts b/src/types/index.ts
index d4001d2..fe10cf3 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -27,6 +27,9 @@ export interface Podcast {
comments_count?: number
is_liked?: boolean
chapters?: Chapter[]
+ show_id?: string | null
+ show?: Show | null
+ published_at?: string | null
}
export interface Tag {
@@ -61,3 +64,40 @@ export interface Follow {
follower_id: string
following_id: string
}
+
+export interface Playlist {
+ id: string
+ user_id: string
+ name: string
+ description: string | null
+ cover_url: string | null
+ is_public: boolean
+ created_at: string
+}
+
+export interface PlaylistItem {
+ id: string
+ playlist_id: string
+ podcast_id: string
+ sort_order: number
+ added_at: string
+}
+
+export interface Notification {
+ id: string
+ user_id: string
+ type: 'new_podcast' | 'like' | 'comment'
+ data: Record
+ read: boolean
+ created_at: string
+}
+
+export interface Show {
+ id: string
+ creator_id: string
+ title: string
+ description: string | null
+ cover_url: string | null
+ created_at: string
+ creator?: Profile
+}
diff --git a/supabase/migrations/006_playlists.sql b/supabase/migrations/006_playlists.sql
new file mode 100644
index 0000000..5e684ee
--- /dev/null
+++ b/supabase/migrations/006_playlists.sql
@@ -0,0 +1,60 @@
+-- Playlists
+CREATE TABLE playlists (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
+ name text NOT NULL,
+ description text,
+ cover_url text,
+ is_public boolean NOT NULL DEFAULT true,
+ created_at timestamptz NOT NULL DEFAULT now()
+);
+
+ALTER TABLE playlists ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Anyone can view public playlists"
+ ON playlists FOR SELECT
+ USING (is_public = true OR user_id = auth.uid());
+
+CREATE POLICY "Users can manage their own playlists"
+ ON playlists FOR ALL
+ USING (user_id = auth.uid())
+ WITH CHECK (user_id = auth.uid());
+
+-- Playlist items
+CREATE TABLE playlist_items (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ playlist_id uuid NOT NULL REFERENCES playlists(id) ON DELETE CASCADE,
+ podcast_id uuid NOT NULL REFERENCES podcasts(id) ON DELETE CASCADE,
+ sort_order integer NOT NULL DEFAULT 0,
+ added_at timestamptz NOT NULL DEFAULT now(),
+ UNIQUE (playlist_id, podcast_id)
+);
+
+ALTER TABLE playlist_items ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Anyone can view items of visible playlists"
+ ON playlist_items FOR SELECT
+ USING (
+ EXISTS (
+ SELECT 1 FROM playlists
+ WHERE playlists.id = playlist_items.playlist_id
+ AND (playlists.is_public = true OR playlists.user_id = auth.uid())
+ )
+ );
+
+CREATE POLICY "Playlist owners can manage items"
+ ON playlist_items FOR ALL
+ USING (
+ EXISTS (
+ SELECT 1 FROM playlists
+ WHERE playlists.id = playlist_items.playlist_id
+ AND playlists.user_id = auth.uid()
+ )
+ )
+ WITH CHECK (
+ EXISTS (
+ SELECT 1 FROM playlists
+ WHERE playlists.id = playlist_items.playlist_id
+ AND playlists.user_id = auth.uid()
+ )
+ );
diff --git a/supabase/migrations/007_notifications.sql b/supabase/migrations/007_notifications.sql
new file mode 100644
index 0000000..a2b322b
--- /dev/null
+++ b/supabase/migrations/007_notifications.sql
@@ -0,0 +1,130 @@
+-- Notifications table
+CREATE TABLE notifications (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
+ type text NOT NULL CHECK (type IN ('new_podcast', 'like', 'comment')),
+ data jsonb NOT NULL DEFAULT '{}',
+ read boolean NOT NULL DEFAULT false,
+ created_at timestamptz NOT NULL DEFAULT now()
+);
+
+CREATE INDEX idx_notifications_user ON notifications(user_id, created_at DESC);
+
+ALTER TABLE notifications ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Users can view their own notifications"
+ ON notifications FOR SELECT
+ USING (user_id = auth.uid());
+
+CREATE POLICY "Users can update their own notifications"
+ ON notifications FOR UPDATE
+ USING (user_id = auth.uid())
+ WITH CHECK (user_id = auth.uid());
+
+-- Trigger: notify followers when a new podcast is created
+CREATE OR REPLACE FUNCTION notify_new_podcast()
+RETURNS TRIGGER AS $$
+DECLARE
+ creator_username text;
+ follower_record RECORD;
+BEGIN
+ -- Skip scheduled podcasts
+ IF NEW.published_at IS NOT NULL AND NEW.published_at > now() THEN
+ RETURN NEW;
+ END IF;
+
+ SELECT username INTO creator_username FROM profiles WHERE id = NEW.creator_id;
+
+ FOR follower_record IN
+ SELECT follower_id FROM follows WHERE following_id = NEW.creator_id
+ LOOP
+ INSERT INTO notifications (user_id, type, data)
+ VALUES (
+ follower_record.follower_id,
+ 'new_podcast',
+ jsonb_build_object(
+ 'podcast_id', NEW.id,
+ 'podcast_title', NEW.title,
+ 'username', COALESCE(creator_username, 'Quelqu''un')
+ )
+ );
+ END LOOP;
+
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE TRIGGER on_podcast_created
+ AFTER INSERT ON podcasts
+ FOR EACH ROW
+ EXECUTE FUNCTION notify_new_podcast();
+
+-- Trigger: notify podcast creator when someone likes their podcast
+CREATE OR REPLACE FUNCTION notify_like()
+RETURNS TRIGGER AS $$
+DECLARE
+ liker_username text;
+ podcast_record RECORD;
+BEGIN
+ SELECT username INTO liker_username FROM profiles WHERE id = NEW.user_id;
+ SELECT id, title, creator_id INTO podcast_record FROM podcasts WHERE id = NEW.podcast_id;
+
+ -- Don't notify if user likes their own podcast
+ IF podcast_record.creator_id = NEW.user_id THEN
+ RETURN NEW;
+ END IF;
+
+ INSERT INTO notifications (user_id, type, data)
+ VALUES (
+ podcast_record.creator_id,
+ 'like',
+ jsonb_build_object(
+ 'podcast_id', podcast_record.id,
+ 'podcast_title', podcast_record.title,
+ 'username', COALESCE(liker_username, 'Quelqu''un')
+ )
+ );
+
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE TRIGGER on_like_created
+ AFTER INSERT ON likes
+ FOR EACH ROW
+ EXECUTE FUNCTION notify_like();
+
+-- Trigger: notify podcast creator when someone comments
+CREATE OR REPLACE FUNCTION notify_comment()
+RETURNS TRIGGER AS $$
+DECLARE
+ commenter_username text;
+ podcast_record RECORD;
+BEGIN
+ SELECT username INTO commenter_username FROM profiles WHERE id = NEW.user_id;
+ SELECT id, title, creator_id INTO podcast_record FROM podcasts WHERE id = NEW.podcast_id;
+
+ -- Don't notify if user comments on their own podcast
+ IF podcast_record.creator_id = NEW.user_id THEN
+ RETURN NEW;
+ END IF;
+
+ INSERT INTO notifications (user_id, type, data)
+ VALUES (
+ podcast_record.creator_id,
+ 'comment',
+ jsonb_build_object(
+ 'podcast_id', podcast_record.id,
+ 'podcast_title', podcast_record.title,
+ 'username', COALESCE(commenter_username, 'Quelqu''un')
+ )
+ );
+
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE TRIGGER on_comment_created
+ AFTER INSERT ON comments
+ FOR EACH ROW
+ EXECUTE FUNCTION notify_comment();
diff --git a/supabase/migrations/008_shows.sql b/supabase/migrations/008_shows.sql
new file mode 100644
index 0000000..0570fc6
--- /dev/null
+++ b/supabase/migrations/008_shows.sql
@@ -0,0 +1,25 @@
+-- Shows / Series
+CREATE TABLE shows (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ creator_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
+ title text NOT NULL,
+ description text,
+ cover_url text,
+ created_at timestamptz NOT NULL DEFAULT now()
+);
+
+ALTER TABLE shows ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Anyone can view shows"
+ ON shows FOR SELECT
+ USING (true);
+
+CREATE POLICY "Creators can manage their own shows"
+ ON shows FOR ALL
+ USING (creator_id = auth.uid())
+ WITH CHECK (creator_id = auth.uid());
+
+-- Add show_id to podcasts
+ALTER TABLE podcasts ADD COLUMN show_id uuid REFERENCES shows(id) ON DELETE SET NULL;
+
+CREATE INDEX idx_podcasts_show ON podcasts(show_id) WHERE show_id IS NOT NULL;
diff --git a/supabase/migrations/009_scheduled_publishing.sql b/supabase/migrations/009_scheduled_publishing.sql
new file mode 100644
index 0000000..6376203
--- /dev/null
+++ b/supabase/migrations/009_scheduled_publishing.sql
@@ -0,0 +1,29 @@
+-- Scheduled publishing
+ALTER TABLE podcasts ADD COLUMN published_at timestamptz;
+
+-- Update the SELECT policy: podcasts are visible if:
+-- 1. Not scheduled (published_at IS NULL)
+-- 2. Scheduled time has passed (published_at <= now())
+-- 3. Current user is the creator
+-- Note: drop the existing select policy and recreate it
+
+-- First find and drop existing select policies on podcasts
+DO $$
+DECLARE
+ pol RECORD;
+BEGIN
+ FOR pol IN
+ SELECT policyname FROM pg_policies
+ WHERE tablename = 'podcasts' AND cmd = 'SELECT'
+ LOOP
+ EXECUTE format('DROP POLICY %I ON podcasts', pol.policyname);
+ END LOOP;
+END $$;
+
+CREATE POLICY "Podcasts visible when published or owned"
+ ON podcasts FOR SELECT
+ USING (
+ published_at IS NULL
+ OR published_at <= now()
+ OR creator_id = auth.uid()
+ );