diff --git a/backend/src/db/api/watch_entries.js b/backend/src/db/api/watch_entries.js index d5bc883..aac5386 100644 --- a/backend/src/db/api/watch_entries.js +++ b/backend/src/db/api/watch_entries.js @@ -399,7 +399,7 @@ module.exports = class Watch_entriesDBApi { }, { - model: db.episodes, + model: db.episodes, include: [{ model: db.seasons, as: 'season' }], as: 'episode', where: filter.episode ? { diff --git a/frontend/src/components/ContinueWatching.tsx b/frontend/src/components/ContinueWatching.tsx new file mode 100644 index 0000000..5f7e13e --- /dev/null +++ b/frontend/src/components/ContinueWatching.tsx @@ -0,0 +1,126 @@ +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import { mdiPlay, mdiCheckCircle, mdiLoading } from '@mdi/js'; +import BaseIcon from './BaseIcon'; +import BaseButton from './BaseButton'; +import CardBox from './CardBox'; +import SectionTitleLineWithButton from './SectionTitleLineWithButton'; +import { useAppSelector } from '../stores/hooks'; + +export default function ContinueWatching() { + const [entries, setEntries] = useState([]); + const [loading, setLoading] = useState(true); + const { currentUser } = useAppSelector((state) => state.auth); + + const fetchContinueWatching = async () => { + try { + setLoading(true); + // Fetch recent watch entries where status is 'watching' + const response = await axios.get('/watch_entries', { + params: { + limit: 3, + page: 0, + status: 'watching' + } + }); + setEntries(response.data.rows || []); + } catch (error) { + console.error('Failed to fetch continue watching:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (currentUser) { + fetchContinueWatching(); + } + }, [currentUser]); + + if (loading) { + return ( + +
+ + Looking for your last watched... +
+
+ ); + } + + if (entries.length === 0) { + return ( +
+ + {''} + + +
+

No active series found. Start your journey by adding a title to your watchlist!

+ +
+
+
+ ); + } + + return ( +
+ + {''} + + +
+ {entries.map((entry: any) => ( + +
+
+
+

+ {entry.title?.name} +

+

+ {entry.title?.title_type || 'Movie'} +

+
+
+ +
+

+ {entry.episode ? `S${entry.episode.season?.season_number || '?'} E${entry.episode.episode_number}: ${entry.episode.name}` : 'Main Movie'} +

+
+
+
+
+ +
+ + +
+
+
+ ))} +
+
+ ); +} diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index b49344a..87f6a5b 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -14,8 +14,10 @@ import { hasPermission } from "../helpers/userPermissions"; import { fetchWidgets } from '../stores/roles/rolesSlice'; import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; import { SmartWidget } from '../components/SmartWidget/SmartWidget'; +import ContinueWatching from '../components/ContinueWatching'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; + const Dashboard = () => { const dispatch = useAppDispatch(); const iconsColor = useAppSelector((state) => state.style.iconsColor); @@ -24,7 +26,6 @@ const Dashboard = () => { const loadingMessage = 'Loading...'; - const [users, setUsers] = React.useState(loadingMessage); const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); @@ -38,29 +39,24 @@ const Dashboard = () => { const [title_tags, setTitle_tags] = React.useState(loadingMessage); const [attachments, setAttachments] = React.useState(loadingMessage); - const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, }); const { currentUser } = useAppSelector((state) => state.auth); const { isFetchingQuery } = useAppSelector((state) => state.openAi); - const { rolesWidgets, loading } = useAppSelector((state) => state.roles); - async function loadData() { const entities = ['users','roles','permissions','franchises','titles','seasons','episodes','watch_entries','watchlist_items','tags','title_tags','attachments',]; const fns = [setUsers,setRoles,setPermissions,setFranchises,setTitles,setSeasons,setEpisodes,setWatch_entries,setWatchlist_items,setTags,setTitle_tags,setAttachments,]; const requests = entities.map((entity, index) => { - if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) { return axios.get(`/${entity.toLowerCase()}/count`); } else { fns[index](null); return Promise.resolve({data: {count: null}}); } - }); Promise.allSettled(requests).then((results) => { @@ -77,6 +73,7 @@ const Dashboard = () => { async function getWidgets(roleId) { await dispatch(fetchWidgets(roleId)); } + React.useEffect(() => { if (!currentUser) return; loadData().then(); @@ -91,9 +88,7 @@ const Dashboard = () => { return ( <> - - {getPageTitle('Overview')} - + {getPageTitle('Overview')} { main> {''} + + {/* Cinematic INITIAL DELTA: Continue Watching Widget */} + {hasPermission(currentUser, 'CREATE_ROLES') && { setWidgetsRole={setWidgetsRole} widgetsRole={widgetsRole} />} + {!!rolesWidgets.length && hasPermission(currentUser, 'CREATE_ROLES') && ( -

+

{`${widgetsRole?.role?.label || 'Users'}'s widgets`}

)}
{(isFetchingQuery || loading) && ( -
+
{ ))}
- {!!rolesWidgets.length &&
} + {!!rolesWidgets.length &&
}
- - {hasPermission(currentUser, 'READ_USERS') && -
+
-
- Users -
-
- {users} -
+
Users
+
{users}
- +
} - - {hasPermission(currentUser, 'READ_ROLES') && -
-
-
-
- Roles -
-
- {roles} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_PERMISSIONS') && -
-
-
-
- Permissions -
-
- {permissions} -
-
-
- -
-
-
- } - + {hasPermission(currentUser, 'READ_FRANCHISES') && -
+
-
- Franchises -
-
- {franchises} -
+
Franchises
+
{franchises}
- +
} - + {hasPermission(currentUser, 'READ_TITLES') && -
+
-
- Titles -
-
- {titles} -
+
Titles
+
{titles}
- +
} - - {hasPermission(currentUser, 'READ_SEASONS') && -
-
-
-
- Seasons -
-
- {seasons} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_EPISODES') && -
-
-
-
- Episodes -
-
- {episodes} -
-
-
- -
-
-
- } - + {hasPermission(currentUser, 'READ_WATCH_ENTRIES') && -
+
-
- Watch entries -
-
- {watch_entries} -
+
Watch entries
+
{watch_entries}
- +
} - + {hasPermission(currentUser, 'READ_WATCHLIST_ITEMS') && -
+
-
- Watchlist items -
-
- {watchlist_items} -
+
Watchlist items
+
{watchlist_items}
- +
} - - {hasPermission(currentUser, 'READ_TAGS') && -
-
-
-
- Tags -
-
- {tags} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_TITLE_TAGS') && -
-
-
-
- Title tags -
-
- {title_tags} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_ATTACHMENTS') && -
-
-
-
- Attachments -
-
- {attachments} -
-
-
- -
-
-
- } - -
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index a68af03..71e5ead 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,96 @@ - import React, { useEffect, useState } from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; import Link from 'next/link'; import BaseButton from '../components/BaseButton'; -import CardBox from '../components/CardBox'; import SectionFullScreen from '../components/SectionFullScreen'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; - +import { mdiMovieOpenStar, mdiPlay } from '@mdi/js'; +import BaseIcon from '../components/BaseIcon'; export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('image'); - const [contentPosition, setContentPosition] = useState('right'); const textColor = useAppSelector((state) => state.style.linkColor); + const title = 'Entertainment Tracker CRM'; - const title = 'Movie & Series Tracking CRM' + return ( +
+ + {getPageTitle('Home')} + - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); + +
+ {/* Cinematic Background Gradient */} +
+ + {/* Hero Content */} +
+
+ + Ultimate Marvel & Star Wars Tracker +
+ +

+ Track Your Cinematic Journey +

+ +

+ The all-in-one CRM for franchise fans. Organize movies, track series progress, and never miss what's next in the galaxy. +

+ +
+ + +
- const imageBlock = (image) => ( -
- + {/* Feature Badges */} +
+ {[ + { label: 'Franchise Tracking', desc: 'Marvel, Star Wars & more' }, + { label: 'Series Progress', desc: 'Episode-by-episode' }, + { label: 'Watchlist', desc: 'Next Up queue' }, + { label: 'Statistics', desc: 'Your viewing habits' } + ].map((feature, i) => ( +
+
{feature.label}
+
{feature.desc}
+
+ ))} +
+
+
+ + +
+
+

© 2026 {title}. All rights reserved.

+
+ + Privacy Policy + + + Terms of Service + +
+
+
); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- - -
) - } - }; - - return ( -
- - {getPageTitle('Starter Page')} - - - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - - - -
-
-
-
-
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
- -
- ); } Starter.getLayout = function getLayout(page: ReactElement) { - return {page}; + return {page}; }; -