diff --git a/package.json b/package.json index f791a63d..4bd49266 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "postcss": "^8.4.28", "tailwindcss": "^3.3.3", "ts-node": "^10.9.2", + "tsx": "^4.21.0", "vitest": "^4.1.0" }, "resolutions": { diff --git a/src/app/api/auth/authOptions.ts b/src/app/api/auth/authOptions.ts index 17fb49c1..b2d28ec6 100644 --- a/src/app/api/auth/authOptions.ts +++ b/src/app/api/auth/authOptions.ts @@ -12,11 +12,10 @@ export const authOptions: NextAuthOptions = { // @ts-expect-error async authorize(credentials) { // @ts-expect-error - const { jwtToken } = credentials; + const { jwtToken, id, email, role, firstName, lastName, phoneNumber, avatar } = credentials; try { - const user = { ...credentials }; // Extract user details from credentials - return { ...user, token: jwtToken }; + return { id, token: jwtToken, email, role, firstName, lastName, phone: phoneNumber, avatar }; } catch { return null; } @@ -28,10 +27,14 @@ export const authOptions: NextAuthOptions = { }, callbacks: { async session({ session, token }) { - if (token.sub && session.user) { + if (session.user) { session.user.id = token.sub; session.user.role = token.role as string; session.role = token.role as string; + session.user.firstName = token.firstName as string | undefined; + session.user.lastName = token.lastName as string | undefined; + session.user.phone = token.phone as string | undefined; + session.user.avatar = token.avatar as string | undefined; } return session; }, @@ -40,11 +43,21 @@ export const authOptions: NextAuthOptions = { token.id = user.id; token.email = user.email; token.role = user.role ?? token.role; + token.firstName = user.firstName; + token.lastName = user.lastName; + token.phone = user.phone; + token.avatar = user.avatar; } if (token.sub) { const db = await dbClient(); if (db) { + const dbUser = await db + .collection(dbCollections.users.name) + .findOne({ _id: new BSON.ObjectId(token.sub) }, { projection: { _id: 1 } }); + + if (!dbUser) return null; // invalidate session if user no longer exists + const roleMapping = await db.collection(dbCollections.user_role_mappings.name).findOne({ user_id: new BSON.ObjectId(token.sub), }); diff --git a/src/app/api/auth/login.ts b/src/app/api/auth/login.ts index 61c057af..ac44da80 100644 --- a/src/app/api/auth/login.ts +++ b/src/app/api/auth/login.ts @@ -42,6 +42,10 @@ export default async function login_(request: Request) { role: 1, is_verified: 1, password: 1, + firstName: 1, + lastName: 1, + phoneNumber: 1, + avatar: 1, }, }, ); @@ -67,17 +71,33 @@ export default async function login_(request: Request) { }); } - const userRole = await db + let userRoleResult = await db .collection(dbCollections.user_role_mappings.name) .aggregate(p_fetchUserRoleDetails({ userId: `${user._id}` })) .toArray(); + if (!userRoleResult[0]) { + const defaultRole = await db + .collection(dbCollections.user_roles.name) + .findOne({ name: 'student' }); + + if (defaultRole) { + await db.collection(dbCollections.user_role_mappings.name).insertOne({ + user_id: user._id, + role_id: defaultRole._id, + created_at: new Date(), + }); + + userRoleResult = [{ role_details: defaultRole }]; + } + } + let role: null | T_RECORD = null; - if (userRole[0]) { + if (userRoleResult[0]) { role = { - id: userRole[0].role_details._id, - name: userRole[0].role_details.name, + id: userRoleResult[0].role_details._id, + name: userRoleResult[0].role_details.name, }; } @@ -92,6 +112,10 @@ export default async function login_(request: Request) { id: user._id.toString(), email: user.email, role: role?.name, + firstName: user.firstName, + lastName: user.lastName, + phoneNumber: user.phoneNumber, + avatar: user.avatar, }, jwtToken: token, }; diff --git a/src/app/api/auth/signup.ts b/src/app/api/auth/signup.ts index 86cb4f8d..03933f9c 100644 --- a/src/app/api/auth/signup.ts +++ b/src/app/api/auth/signup.ts @@ -87,7 +87,20 @@ export default async function signup_(request: Request) { userDoc.grade = grade; } - await db.collection(dbCollections.users.name).insertOne(userDoc); + const studentRole = await db.collection(dbCollections.user_roles.name).findOne({ name: 'student' }); + if (!studentRole) { + return new Response(JSON.stringify({ isError: true, code: SPARKED_PROCESS_CODES.DB_CONNECTION_FAILED }), { + status: HttpStatusCode.InternalServerError, + }); + } + + const insertResult = await db.collection(dbCollections.users.name).insertOne(userDoc); + + await db.collection(dbCollections.user_role_mappings.name).insertOne({ + user_id: insertResult.insertedId, + role_id: studentRole._id, + created_at: new Date(), + }); await resend.emails.send({ from: 'Sparked Support ', diff --git a/src/app/api/lib/db/collections.ts b/src/app/api/lib/db/collections.ts index df83faba..8350df0b 100644 --- a/src/app/api/lib/db/collections.ts +++ b/src/app/api/lib/db/collections.ts @@ -35,7 +35,7 @@ export const dbCollections: T_dbCollection = { }, page_links: { name: 'page-links', - label: 'Media Content', + label: 'Page Links', }, grades: { name: 'grades', @@ -73,4 +73,12 @@ export const dbCollections: T_dbCollection = { name: 'media_reactions', label: 'Media Reactions', }, + institution_memberships: { + name: 'institution_memberships', + label: 'Institution Memberships', + }, + institution_invites: { + name: 'institution_invites', + label: 'Institution Invites', + }, } as const; diff --git a/src/app/api/lib/db/index.ts b/src/app/api/lib/db/index.ts index 533f96d8..70d8135b 100644 --- a/src/app/api/lib/db/index.ts +++ b/src/app/api/lib/db/index.ts @@ -1,27 +1,39 @@ -import { MongoClient } from "mongodb"; +import { MongoClient } from 'mongodb'; if (!process.env.MONGODB_URI) { console.error('Invalid/Missing environment variable: "MONGODB_URI"'); } -const uri = process.env.MONGODB_URI || "mongodb://...."; // Just to allow the build to pass -const options = {}; +const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017/sparked'; // Just to allow the build to pass +const options = { + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, +}; declare global { // eslint-disable-next-line no-var var _mongoClientPromise: Promise | undefined; } -if (!global._mongoClientPromise) { - const client = new MongoClient(uri, options); - global._mongoClientPromise = client.connect(); +function getClientPromise(): Promise { + if (!global._mongoClientPromise) { + const client = new MongoClient(uri, options); + global._mongoClientPromise = client.connect().catch((err) => { + // Reset so the next request gets a fresh attempt + global._mongoClientPromise = undefined; + return Promise.reject(err); + }); + } + return global._mongoClientPromise; } -const mongoClientPromise = global._mongoClientPromise; - export const dbClient = async () => { - const client = await mongoClientPromise; - return client.db(process.env.MONGODB_DB); + try { + const client = await getClientPromise(); + return client.db(process.env.MONGODB_DB); + } catch { + return null; + } }; -export default mongoClientPromise; +export default getClientPromise(); diff --git a/src/app/api/lib/db/init.ts b/src/app/api/lib/db/init.ts index 9e406565..3c722b0c 100644 --- a/src/app/api/lib/db/init.ts +++ b/src/app/api/lib/db/init.ts @@ -1,5 +1,8 @@ -import { addDefaultRoles } from './migrations/addDefaultRoles'; +import { dbClient } from '.'; +import { runMigrations } from './migrate'; export async function initializeDatabase() { - await addDefaultRoles(); + const db = await dbClient(); + if (!db) return; + await runMigrations(db); } diff --git a/src/app/api/lib/db/migrations/addDefaultRoles.ts b/src/app/api/lib/db/migrations/addDefaultRoles.ts deleted file mode 100644 index 7a2059d5..00000000 --- a/src/app/api/lib/db/migrations/addDefaultRoles.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { dbClient } from '../'; -import { dbCollections } from '../collections'; - -const DEFAULT_ROLES = [ - { - name: 'Admin', - description: 'Administrator with full access', - }, - { - name: 'Content Manager', - description: 'Manager with full content management access', - }, - { - name: 'Editor', - description: 'Editor with limited content management access', - }, - { - name: 'student', - description: 'Student with limited access', - }, -]; - -export async function addDefaultRoles() { - const db = await dbClient(); - if (!db) return; - - for (const role of DEFAULT_ROLES) { - const exists = await db.collection(dbCollections.user_roles.name).findOne({ name: role.name }); - if (!exists) { - await db.collection(dbCollections.user_roles.name).insertOne({ - ...role, - created_at: new Date(), - updated_at: new Date(), - }); - } - } -} diff --git a/src/app/api/lib/db/schema.ts b/src/app/api/lib/db/schema.ts index ec3babc8..01b9022a 100644 --- a/src/app/api/lib/db/schema.ts +++ b/src/app/api/lib/db/schema.ts @@ -72,7 +72,7 @@ export const coursesSchema = { code: 'string', created_at: 'date', created_by_id: 'objectId', - institutions_id: 'objectId?', + institution_id: 'objectId?', is_visible: 'bool?', languages: 'courses_languages []', name: 'string', @@ -97,7 +97,7 @@ export const courses_languagesSchema = { }; export type feed_back = { - _id?: ObjectId; + _id: ObjectId; attachment_link?: string; created_at?: Date; message?: string; @@ -108,7 +108,7 @@ export type feed_back = { export const feed_backSchema = { name: 'feed_back', properties: { - _id: 'objectId?', + _id: 'objectId', attachment_link: 'string?', created_at: 'date?', message: 'string?', @@ -185,6 +185,52 @@ export const institutionsSchema = { primaryKey: '_id', }; +export type institution_memberships = { + _id: ObjectId; + user_id: ObjectId; + institution_id: ObjectId; + role: 'admin' | 'member'; + joined_at: Date; + invited_by_id?: ObjectId; +}; + +export const institution_membershipsSchema = { + name: 'institution_memberships', + properties: { + _id: 'objectId', + user_id: 'objectId', + institution_id: 'objectId', + role: 'string', + joined_at: 'date', + invited_by_id: 'objectId?', + }, + primaryKey: '_id', +}; + +export type institution_invites = { + _id: ObjectId; + institution_id: ObjectId; + email: string; + token: string; + invited_by_id: ObjectId; + expires_at: Date; + used_at?: Date; +}; + +export const institution_invitesSchema = { + name: 'institution_invites', + properties: { + _id: 'objectId', + institution_id: 'objectId', + email: 'string', + token: 'string', + invited_by_id: 'objectId', + expires_at: 'date', + used_at: 'date?', + }, + primaryKey: '_id', +}; + // Keep schools schema for backward compatibility (will be migrated to institutions) export const schoolsSchema = { name: 'schools', @@ -207,7 +253,7 @@ export type preferences = { description?: string; title?: string; updated_at?: Date; - updated_by_id?: Date; + updated_by_id?: ObjectId; }; export const preferencesSchema = { @@ -219,7 +265,7 @@ export const preferencesSchema = { description: 'string?', title: 'string?', updated_at: 'date?', - updated_by_id: 'date?', + updated_by_id: 'objectId?', }, primaryKey: '_id', }; @@ -230,7 +276,7 @@ export type settings = { created_by_id: ObjectId; description?: string; title: string; - upated_at?: Date; + updated_at?: Date; updated_by_id?: ObjectId; }; @@ -242,7 +288,7 @@ export const settingsSchema = { created_by_id: 'objectId', description: 'string?', title: 'string', - upated_at: 'date?', + updated_at: 'date?', updated_by_id: 'objectId?', }, primaryKey: '_id', @@ -281,7 +327,7 @@ export type user_roles = { description?: string; label?: string; name?: string; - permission_ids: user_permissions[]; + permission_ids: ObjectId[]; updated_at?: Date; updated_by_id?: ObjectId; }; @@ -295,7 +341,7 @@ export const user_rolesSchema = { description: 'string?', label: 'string?', name: 'string?', - permission_ids: 'user_permissions[]', + permission_ids: 'objectId[]', updated_at: 'date?', updated_by_id: 'objectId?', }, diff --git a/src/app/api/lib/db/types.ts b/src/app/api/lib/db/types.ts index 6a065b79..4e8229dc 100644 --- a/src/app/api/lib/db/types.ts +++ b/src/app/api/lib/db/types.ts @@ -72,4 +72,12 @@ export type T_dbCollection = { name: string; label: string; }; + institution_memberships: { + name: string; + label: string; + }; + institution_invites: { + name: string; + label: string; + }; }; diff --git a/src/app/api/media-content/index.ts b/src/app/api/media-content/index.ts index 336082b0..935cb84d 100644 --- a/src/app/api/media-content/index.ts +++ b/src/app/api/media-content/index.ts @@ -156,6 +156,7 @@ export async function fetchMediaContentById_(request: any) { query: { _id: new BSON.ObjectId(mediaContentId), }, + limit: 1, }), ) .toArray(); diff --git a/src/app/api/media-content/pipelines.ts b/src/app/api/media-content/pipelines.ts index 0b589c78..9b1cf429 100644 --- a/src/app/api/media-content/pipelines.ts +++ b/src/app/api/media-content/pipelines.ts @@ -184,7 +184,7 @@ export const p_fetchRandomMediaContent = ({ }, }, { - $unwind: '$user', + $unwind: { path: '$user', preserveNullAndEmptyArrays: true }, }, { $lookup: { diff --git a/src/app/api/users/edit.ts b/src/app/api/users/edit.ts index c26d468e..1af12b91 100644 --- a/src/app/api/users/edit.ts +++ b/src/app/api/users/edit.ts @@ -1,24 +1,31 @@ import SPARKED_PROCESS_CODES from 'app/shared/processCodes'; import { BSON } from 'mongodb'; import { Session } from 'next-auth'; -import { zfd } from 'zod-form-data'; +import { z } from 'zod'; import { dbClient } from '../lib/db'; import { dbCollections } from '../lib/db/collections'; import { default as USER_PROCESS_CODES } from './processCodes'; import { HttpStatusCode } from 'axios'; export default async function editUser_(request: Request, session?: Session) { - const schema = zfd.formData({ - _id: zfd.text(), - email: zfd.text(), - firstName: zfd.text(), - lastName: zfd.text(), - phoneNumber: zfd.text(), - role: zfd.text().optional(), + const schema = z.object({ + _id: z.string(), + email: z.string(), + firstName: z.string(), + lastName: z.string(), + phoneNumber: z.string(), + role: z.string().optional(), }); const formBody = await request.json(); - const { _id, email, firstName, lastName, role, phoneNumber } = schema.parse(formBody); + const parsed = schema.safeParse(formBody); + if (!parsed.success) { + return new Response( + JSON.stringify({ isError: true, code: USER_PROCESS_CODES.INVALID_INPUT }), + { status: HttpStatusCode.BadRequest }, + ); + } + const { _id, email, firstName, lastName, role, phoneNumber } = parsed.data; try { const db = await dbClient(); diff --git a/src/app/api/users/processCodes.ts b/src/app/api/users/processCodes.ts index 15c94bc2..fed60885 100644 --- a/src/app/api/users/processCodes.ts +++ b/src/app/api/users/processCodes.ts @@ -8,6 +8,7 @@ const USER_PROCESS_CODES = { USER_CREATED: 6503, USER_DELETED: 6504, INVALID_ROLE: 7005, + INVALID_INPUT: 6505, } satisfies TProcessCode; export default USER_PROCESS_CODES; diff --git a/src/components/library/MediaContentPlayer.tsx b/src/components/library/MediaContentPlayer.tsx index 81057efd..f883bdc7 100644 --- a/src/components/library/MediaContentPlayer.tsx +++ b/src/components/library/MediaContentPlayer.tsx @@ -1,7 +1,6 @@ 'use client'; -import { useCallback, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { useCallback, useRef, useState } from 'react'; import { T_RawMediaContentFields } from 'types/media-content'; import { fetcher } from '@hooks/use-swr/fetcher'; import { API_LINKS } from 'app/links'; @@ -16,31 +15,51 @@ type Props = { initialRelatedMedia: T_RawMediaContentFields[] | null; }; +async function fetchFullMedia(id: string) { + return fetcher<{ mediaContent: T_RawMediaContentFields }>( + API_LINKS.FETCH_MEDIA_CONTENT_BY_ID + NETWORK_UTILS.formatGetParams({ mediaContentId: id, withMetaData: 'true' }), + ); +} + export function MediaContentPlayer({ initialMediaContent, initialRelatedMedia }: Props) { - const router = useRouter(); const [activeMedia, setActiveMedia] = useState(initialMediaContent); const [relatedMedia, setRelatedMedia] = useState(initialRelatedMedia); - const handleSelect = useCallback( - async (item: T_RawMediaContentFields) => { - // Instantly swap to basic data from the list — no flash - setActiveMedia(item); - router.replace(`/library/media/${item._id}`, { scroll: false }); - - // Background: fetch full metadata - const fullResult = await fetcher<{ mediaContent: T_RawMediaContentFields }>( - API_LINKS.FETCH_MEDIA_CONTENT_BY_ID + NETWORK_UTILS.formatGetParams({ mediaContentId: item._id, withMetaData: 'true' }), - ); - if (!(fullResult instanceof Error)) { - setActiveMedia(fullResult.mediaContent); - // Background: update related list for new active item - const related = await fetchRelatedMediaClient(fullResult.mediaContent); - setRelatedMedia(related); - } - }, - [router], + // Cache fetched media by ID — re-selecting a visited item is instant + const mediaCache = useRef>( + new Map([[initialMediaContent._id, initialMediaContent]]), ); + const handleSelect = useCallback(async (item: T_RawMediaContentFields) => { + setActiveMedia(item); + window.history.replaceState(null, '', `/library/media/${item._id}`); + + const cached = mediaCache.current.get(item._id); + if (cached) { + setActiveMedia(cached); + const related = await fetchRelatedMediaClient(cached); + setRelatedMedia(related); + return; + } + + const fullResult = await fetchFullMedia(item._id); + if (!(fullResult instanceof Error)) { + mediaCache.current.set(fullResult.mediaContent._id, fullResult.mediaContent); + setActiveMedia(fullResult.mediaContent); + const related = await fetchRelatedMediaClient(fullResult.mediaContent); + setRelatedMedia(related); + } + }, []); + + // Prefetch full metadata on hover so click feels instant + const handlePrefetch = useCallback(async (item: T_RawMediaContentFields) => { + if (mediaCache.current.has(item._id)) return; + const result = await fetchFullMedia(item._id); + if (!(result instanceof Error)) { + mediaCache.current.set(result.mediaContent._id, result.mediaContent); + } + }, []); + return (
@@ -51,6 +70,7 @@ export function MediaContentPlayer({ initialMediaContent, initialRelatedMedia }: relatedMediaContent={relatedMedia} activeMediaId={activeMedia._id} onSelect={handleSelect} + onPrefetch={handlePrefetch} />
); diff --git a/src/components/library/RelatedMediaContentList.tsx b/src/components/library/RelatedMediaContentList.tsx index 02d584db..19215a3f 100644 --- a/src/components/library/RelatedMediaContentList.tsx +++ b/src/components/library/RelatedMediaContentList.tsx @@ -12,12 +12,16 @@ const isValidImage = (url: string) => { const RelatedMediaItem = memo( ({ item, + index, isActive, onSelect, + onPrefetch, }: { item: T_RawMediaContentFields; + index: number; isActive: boolean; onSelect?: (item: T_RawMediaContentFields) => void; + onPrefetch?: (item: T_RawMediaContentFields) => void; }) => { const domainName = item.external_url ? new URL(item.external_url).hostname : ''; const placeholderImage = `https://placehold.co/120x100?text=${domainName || item.name}`; @@ -31,7 +35,7 @@ const RelatedMediaItem = memo( width={120} height={90} className="object-cover rounded" - loading="eager" + loading={index < 3 ? 'eager' : 'lazy'} />

@@ -45,7 +49,11 @@ const RelatedMediaItem = memo( if (onSelect) { return (
  • -
  • @@ -66,10 +74,12 @@ export function RelatedMediaContentList({ relatedMediaContent, activeMediaId, onSelect, + onPrefetch, }: { relatedMediaContent: T_RawMediaContentFields[] | null; activeMediaId?: string; onSelect?: (item: T_RawMediaContentFields) => void; + onPrefetch?: (item: T_RawMediaContentFields) => void; }) { return (
    @@ -77,12 +87,14 @@ export function RelatedMediaContentList({ <>

    Related Media

      - {relatedMediaContent.map((item) => ( + {relatedMediaContent.map((item, index) => ( ))}
    diff --git a/src/components/logo/index.tsx b/src/components/logo/index.tsx index da6768bb..4e8de2bc 100644 --- a/src/components/logo/index.tsx +++ b/src/components/logo/index.tsx @@ -11,6 +11,7 @@ const AppLogo = ({ scale = 1 }: { scale?: number }) => { src="/alternate-logo.svg" alt="SparkEd Logo" className="admin-logo" + priority /> ); }; diff --git a/src/hocs/withAuthorization.tsx b/src/hocs/withAuthorization.tsx index cde7bd48..c2942f13 100644 --- a/src/hocs/withAuthorization.tsx +++ b/src/hocs/withAuthorization.tsx @@ -27,8 +27,6 @@ type ExtendedSession = { }; }; - - export function withAuthorization

    ( WrappedComponent: ComponentType

    , { requireAdmin = false, requireGuest = false }: Options = {}, @@ -41,6 +39,8 @@ export function withAuthorization

    ( status: 'loading' | 'authenticated' | 'unauthenticated'; }; + console.log(session); + const user = useUser(); const setUser = useSetUser(); const clearUser = useClearUser(); @@ -60,15 +60,17 @@ export function withAuthorization

    ( setLoading(false); if (status === 'authenticated' && session?.user) { - // Only sync session to store if user is not already set - // This prevents overwriting the correct isAdmin value set during login + const sessionRole = session.user.role?.toLowerCase() as 'student' | 'user' | 'admin'; + const sessionIsAdmin = sessionRole === 'admin'; if (!user) { - const sessionUser = { + setUser({ ...session.user, - role: session.user.role as 'student' | 'user' | 'admin', - isAdmin: session.user.role?.toLowerCase() === 'admin', - }; - setUser(sessionUser); + role: sessionRole, + isAdmin: sessionIsAdmin, + }); + } else if (user.role?.toLowerCase() !== sessionRole || user.isAdmin !== sessionIsAdmin) { + // Role changed (e.g. user was promoted to admin) — sync from session + setUser({ ...user, role: sessionRole, isAdmin: sessionIsAdmin }); } } else if (status === 'unauthenticated') { if (user) { @@ -107,6 +109,7 @@ export function withAuthorization

    ( return; } + console.log(isAdmin, requireAdmin); // If requires admin and user is not admin, redirect to library if (requireAdmin && !isAdmin) { hasRedirected.current = true; diff --git a/src/hooks/useAuth/index.ts b/src/hooks/useAuth/index.ts index 9bd0a902..8d405505 100644 --- a/src/hooks/useAuth/index.ts +++ b/src/hooks/useAuth/index.ts @@ -90,26 +90,32 @@ const useAuth = () => { const decodedToken: { role?: { name: string } } = jwtDecode(jwtToken); const userRole: typeof decodedToken.role = decodedToken.role || { name: 'student' }; + const { user: loginUser } = responseData; + const singInResp = await signIn('credentials', { redirect: false, jwtToken, + id: loginUser?.id ?? '', email: fields.email, role: (userRole?.name ?? 'user') as 'student' | 'user' | 'admin', + firstName: loginUser?.firstName ?? '', + lastName: loginUser?.lastName ?? '', + phoneNumber: loginUser?.phoneNumber ?? '', + avatar: loginUser?.avatar ?? '', }); - + const isUserAdmin = userRole?.name?.toLowerCase() === 'admin'; if (singInResp?.ok && !singInResp?.error) { - const userData = { email: fields.email, - firstName: responseData.firstName, - lastName: responseData.lastName, - phone: responseData.phoneNumber, + firstName: loginUser?.firstName, + lastName: loginUser?.lastName, + phone: loginUser?.phoneNumber, role: userRole?.name as 'student' | 'user' | 'admin', isAdmin: isUserAdmin, }; - + setUser(userData); } diff --git a/src/hooks/useMediaInteractions.ts b/src/hooks/useMediaInteractions.ts index 2a5ec2e1..1079b2f6 100644 --- a/src/hooks/useMediaInteractions.ts +++ b/src/hooks/useMediaInteractions.ts @@ -3,9 +3,12 @@ import { Session } from 'next-auth'; import { useFetch } from './use-swr'; export function useMediaInteractions(mediaId: string) { - const [hasRecordedView, setHasRecordedView] = useState(false); + // Track which mediaId has been viewed so the flag resets on media switch + const [recordedMediaId, setRecordedMediaId] = useState(null); const [isLoading, setIsLoading] = useState(false); + const hasRecordedView = recordedMediaId === mediaId; + const { data: viewCountData } = useFetch(`/api/media-actions/getViewCount?mediaId=${mediaId}`); const { @@ -30,7 +33,7 @@ export function useMediaInteractions(mediaId: string) { }), }); - setHasRecordedView(true); + setRecordedMediaId(mediaId); } catch (error) { console.error('Error recording view:', error); } finally { diff --git a/src/types/media-content/index.ts b/src/types/media-content/index.ts index d6b109ef..490704f9 100644 --- a/src/types/media-content/index.ts +++ b/src/types/media-content/index.ts @@ -41,6 +41,8 @@ export type T_RawMediaContentFields = { created_at: string; updated_at: string; + institution_id?: string; + external_url?: string; user?: { _id: string; diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts index 0574644d..804f7dc0 100644 --- a/src/types/next-auth.d.ts +++ b/src/types/next-auth.d.ts @@ -34,5 +34,9 @@ declare module 'next-auth/jwt' { id?: string; role?: string; sub?: string; + firstName?: string; + lastName?: string; + phone?: string; + avatar?: string; } }