From 887ad0f1d443e4fdbc266ae6f04261cc69f2f47d Mon Sep 17 00:00:00 2001 From: motirebuma Date: Thu, 23 Apr 2026 13:05:46 -0400 Subject: [PATCH 1/5] fix some error for the create quote --- .../resolvers/mutations/group/createGroup.ts | 30 +++ .../data/resolvers/mutations/post/addPost.ts | 86 +++++++++ .../data/resolvers/queries/group/getGroups.ts | 15 ++ quotevote-backend/app/server.ts | 97 +++++++--- .../app/dashboard/explore/ExploreContent.tsx | 3 +- .../src/app/dashboard/layout.tsx | 3 +- .../src/components/Navbars/MainNavBar.tsx | 3 +- .../src/components/Sidebar/Sidebar.tsx | 3 +- .../components/SubmitPost/SubmitPostForm.tsx | 9 +- .../src/components/ui/combobox.tsx | 173 +++++++++++------- 10 files changed, 332 insertions(+), 90 deletions(-) create mode 100644 quotevote-backend/app/data/resolvers/mutations/group/createGroup.ts create mode 100644 quotevote-backend/app/data/resolvers/mutations/post/addPost.ts create mode 100644 quotevote-backend/app/data/resolvers/queries/group/getGroups.ts diff --git a/quotevote-backend/app/data/resolvers/mutations/group/createGroup.ts b/quotevote-backend/app/data/resolvers/mutations/group/createGroup.ts new file mode 100644 index 00000000..2daf2f72 --- /dev/null +++ b/quotevote-backend/app/data/resolvers/mutations/group/createGroup.ts @@ -0,0 +1,30 @@ +import { logger } from '~/data/utils/logger'; +import Group from '~/data/models/Group'; +import type { ResolverFn } from '~/types/graphql'; +import type * as Common from '~/types/common'; + +export const createGroup: ResolverFn = async ( + _, + args +) => { + logger.info('Function: createGroup'); + try { + const isExist = await Group.findOne({ title: args.group.title }); + if (isExist) throw new Error('Group name already exists!'); + + const newGroup = await new Group({ + ...args.group, + allowedUserIds: [args.group.creatorId], + adminIds: [args.group.creatorId], + created: new Date(), + }).save(); + + const url = `/${newGroup.title.replace(/ /g, '-').toLowerCase()}`; + await Group.findByIdAndUpdate(newGroup._id, { url }); + newGroup.url = url; + + return newGroup as unknown as Common.Group; + } catch (err) { + throw new Error(err instanceof Error ? err.message : String(err)); + } +}; diff --git a/quotevote-backend/app/data/resolvers/mutations/post/addPost.ts b/quotevote-backend/app/data/resolvers/mutations/post/addPost.ts new file mode 100644 index 00000000..34e0b4cc --- /dev/null +++ b/quotevote-backend/app/data/resolvers/mutations/post/addPost.ts @@ -0,0 +1,86 @@ +import { logger } from '~/data/utils/logger'; +import { logActivity } from '~/data/resolvers/utils/activities'; +import Group from '~/data/models/Group'; +import Post from '~/data/models/Post'; +import MessageRoom from '~/data/models/MessageRoom'; +import type { ResolverFn } from '~/types/graphql'; +import type * as Common from '~/types/common'; + +const URL_REGEX = /(?:https?:\/\/|ftp:\/\/|www\.)[^\s/$.?#].[^\s]*/gi; +const EMOJI_REGEX = + /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]/u; +const INVALID_URL_CHARS_REGEX = /[^a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=%]/; + +function sanitizeUrl(url: string): string | null { + if (!url) return null; + const trimmed = url.trim(); + if (EMOJI_REGEX.test(trimmed)) return null; + if (INVALID_URL_CHARS_REGEX.test(trimmed)) return null; + try { + const parsed = new URL(trimmed); + if (!['http:', 'https:', 'ftp:'].includes(parsed.protocol)) return null; + if (!parsed.hostname || parsed.hostname.length < 3) return null; + return parsed.href; + } catch { + return null; + } +} + +export const addPost: ResolverFn = async ( + _, + args +) => { + logger.info('Function: addPost', { args }); + + URL_REGEX.lastIndex = 0; + if (URL_REGEX.test(args.post.text)) { + logger.warn('Post rejected: URL detected in body', { text: args.post.text }); + throw new Error('Post body cannot contain links. Please use the Citation field.'); + } + + let sanitizedCitationUrl: string | null = null; + if (args.post.citationUrl) { + sanitizedCitationUrl = sanitizeUrl(args.post.citationUrl); + if (!sanitizedCitationUrl) { + logger.warn('Post rejected: Invalid citationUrl', { citationUrl: args.post.citationUrl }); + throw new Error( + 'Invalid citation URL. Only standard URL characters are allowed (no emojis or special characters).' + ); + } + } + + const group = await Group.findById(args.post.groupId); + if (!group) { + throw new Error('Group not found. Please select a valid group.'); + } + + const titleSlug = args.post.title.replace(/ /g, '-').toLowerCase(); + + try { + const newPost = await new Post({ + ...args.post, + url: '', + citationUrl: sanitizedCitationUrl, + }).save(); + + const url = `/post${group.url}/${titleSlug}/${newPost._id}`; + await Post.findByIdAndUpdate(newPost._id, { url }); + newPost.url = url; + + await MessageRoom.create({ + users: [newPost.userId], + postId: newPost._id, + messageType: 'POST', + }); + + await logActivity( + 'POSTED', + { postId: String(newPost._id), userId: String(newPost.userId) }, + newPost.title + ); + + return newPost as unknown as Common.Post; + } catch (err) { + throw new Error(err instanceof Error ? err.message : String(err)); + } +}; diff --git a/quotevote-backend/app/data/resolvers/queries/group/getGroups.ts b/quotevote-backend/app/data/resolvers/queries/group/getGroups.ts new file mode 100644 index 00000000..91b4dc24 --- /dev/null +++ b/quotevote-backend/app/data/resolvers/queries/group/getGroups.ts @@ -0,0 +1,15 @@ +import Group from '~/data/models/Group'; +import type { ResolverFn } from '~/types/graphql'; +import type * as Common from '~/types/common'; + +export const getGroups: ResolverFn = async ( + _, + args +) => { + const query = Group.find({}); + if (args.limit && args.limit > 0) { + query.limit(args.limit); + } + const groups = await query.exec(); + return groups as unknown as Common.Group[]; +}; diff --git a/quotevote-backend/app/server.ts b/quotevote-backend/app/server.ts index 451b4fd0..e8e1235b 100644 --- a/quotevote-backend/app/server.ts +++ b/quotevote-backend/app/server.ts @@ -7,6 +7,9 @@ import mongoose from 'mongoose'; import dotenv from 'dotenv'; import { GraphQLError } from 'graphql'; import { solidResolvers } from './data/resolvers/solidResolvers'; +import { addPost } from './data/resolvers/mutations/post/addPost'; +import { createGroup } from './data/resolvers/mutations/group/createGroup'; +import { getGroups } from './data/resolvers/queries/group/getGroups'; import type { GraphQLContext, PubSub } from './types/graphql'; import { requireAuth } from './data/utils/requireAuth'; import { pubsub } from './data/utils/pubsub'; @@ -44,19 +47,68 @@ async function startServer() { // 2. Apollo Server Setup (v4/v5 Syntax) const server = new ApolloServer({ typeDefs: ` + scalar JSON + scalar Date + type Query { hello: String status: String solidConnectionStatus: SolidConnectionStatus + groups(limit: Int): [Group] } type Mutation { - solidStartConnect(issuer: String!): SolidConnectResult - solidFinishConnect(code: String!, state: String!, redirectUri: String!): SolidConnectResult - solidDisconnect: Boolean - solidPullPortableState: PortableState - solidPushPortableState(input: PortableStateInput!): Boolean - solidAppendActivityEvent(input: ActivityEventInput!): Boolean + addPost(post: PostInput!): Post + createGroup(group: GroupInput!): Group + + solidStartConnect(issuer: String!): SolidConnectResult + solidFinishConnect(code: String!, state: String!, redirectUri: String!): SolidConnectResult + solidDisconnect: Boolean + solidPullPortableState: PortableState + solidPushPortableState(input: PortableStateInput!): Boolean + solidAppendActivityEvent(input: ActivityEventInput!): Boolean + } + + type Group { + _id: String! + creatorId: String! + adminIds: [String] + allowedUserIds: [String] + privacy: String! + title: String! + url: String + description: String + created: String + } + + type Post { + _id: String + userId: String + groupId: String + title: String + text: String + url: String + citationUrl: String + upvotes: Int + downvotes: Int + deleted: Boolean + created: String + } + + input PostInput { + userId: String! + groupId: String! + title: String! + text: String! + citationUrl: String + } + + input GroupInput { + creatorId: String! + title: String! + description: String! + privacy: String! + url: String } type SolidConnectionStatus { @@ -67,29 +119,27 @@ async function startServer() { } type SolidConnectResult { - authorizationUrl: String - success: Boolean - webId: String - issuer: String - message: String + authorizationUrl: String + success: Boolean + webId: String + issuer: String + message: String } - - scalar JSON type PortableState { - version: String - collections: [JSON] + version: String + collections: [JSON] } input PortableStateInput { - version: String - collections: [JSON] + version: String + collections: [JSON] } - + input ActivityEventInput { - type: String! - payload: JSON! - timestamp: String + type: String! + payload: JSON! + timestamp: String } `, resolvers: [ @@ -97,6 +147,11 @@ async function startServer() { Query: { hello: () => 'Hello from TypeScript Backend! 🚀', status: () => 'Active', + groups: getGroups, + }, + Mutation: { + addPost, + createGroup, }, }, solidResolvers diff --git a/quotevote-frontend/src/app/dashboard/explore/ExploreContent.tsx b/quotevote-frontend/src/app/dashboard/explore/ExploreContent.tsx index 35c87c55..a086ff61 100644 --- a/quotevote-frontend/src/app/dashboard/explore/ExploreContent.tsx +++ b/quotevote-frontend/src/app/dashboard/explore/ExploreContent.tsx @@ -3,7 +3,7 @@ import { useState, useCallback } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import Image from 'next/image' -import { Dialog, DialogContent } from '@/components/ui/dialog' +import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog' import PaginatedPostsList from '@/components/Post/PaginatedPostsList' import { useAppStore } from '@/store' import SearchGuestSections from '@/components/SearchContainer/SearchGuestSections' @@ -224,6 +224,7 @@ export default function ExploreContent() { {/* Create Quote Dialog */} + Create Quote diff --git a/quotevote-frontend/src/app/dashboard/layout.tsx b/quotevote-frontend/src/app/dashboard/layout.tsx index c05236dc..4c5fc2cd 100644 --- a/quotevote-frontend/src/app/dashboard/layout.tsx +++ b/quotevote-frontend/src/app/dashboard/layout.tsx @@ -42,7 +42,7 @@ import { DropdownMenuItem, DropdownMenuSeparator, } from '@/components/ui/dropdown-menu'; -import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; import { SubmitPost } from '@/components/SubmitPost/SubmitPost'; /* ------------------------------------------------------------------ */ @@ -514,6 +514,7 @@ export default function DashboardLayout({ + Create Quote diff --git a/quotevote-frontend/src/components/Navbars/MainNavBar.tsx b/quotevote-frontend/src/components/Navbars/MainNavBar.tsx index 1ab995ca..6fa4fa2b 100644 --- a/quotevote-frontend/src/components/Navbars/MainNavBar.tsx +++ b/quotevote-frontend/src/components/Navbars/MainNavBar.tsx @@ -15,7 +15,7 @@ import { SheetHeader, SheetTitle, } from '@/components/ui/sheet'; -import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; import { DisplayAvatar } from '@/components/DisplayAvatar'; import { NotificationMenu } from '@/components/Notifications/NotificationMenu'; import ChatMenu from '@/components/Chat/ChatMenu'; @@ -337,6 +337,7 @@ export function MainNavBar({}: MainNavBarProps) { {/* Create Quote Dialog */} + Create Quote diff --git a/quotevote-frontend/src/components/Sidebar/Sidebar.tsx b/quotevote-frontend/src/components/Sidebar/Sidebar.tsx index 9de1afa5..57767b69 100644 --- a/quotevote-frontend/src/components/Sidebar/Sidebar.tsx +++ b/quotevote-frontend/src/components/Sidebar/Sidebar.tsx @@ -14,7 +14,7 @@ import { Sheet, SheetContent, } from '@/components/ui/sheet'; -import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; import { DisplayAvatar } from '@/components/DisplayAvatar'; import { NotificationMenu } from '@/components/Notifications/NotificationMenu'; import ChatMenu from '@/components/Chat/ChatMenu'; @@ -366,6 +366,7 @@ export function Sidebar({ {/* Create Quote Dialog */} + Create Quote diff --git a/quotevote-frontend/src/components/SubmitPost/SubmitPostForm.tsx b/quotevote-frontend/src/components/SubmitPost/SubmitPostForm.tsx index 97d7843e..4cce10fa 100644 --- a/quotevote-frontend/src/components/SubmitPost/SubmitPostForm.tsx +++ b/quotevote-frontend/src/components/SubmitPost/SubmitPostForm.tsx @@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from 'react' import { useRouter } from 'next/navigation' +import { toast } from 'sonner' import { useForm, Controller } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useMutation, useApolloClient } from '@apollo/client/react' @@ -136,13 +137,14 @@ export function SubmitPostForm({ options = [], user, setOpen }: SubmitPostFormPr }, }) - const { _id } = - (submitResult.data as { addPost?: { _id: string; url: string } })?.addPost || {} + const addPostResult = (submitResult.data as { addPost?: { _id: string; url: string } })?.addPost + const { _id } = addPostResult || {} if (_id) { setSelectedPost(_id) apolloClient.cache.evict({ fieldName: 'posts' }) apolloClient.cache.gc() setOpen(false) + toast.success('Post created!', { description: 'Your quote has been published.' }) router.push('/dashboard/explore') } } catch (err) { @@ -157,12 +159,13 @@ export function SubmitPostForm({ options = [], user, setOpen }: SubmitPostFormPr const hideAlert = () => { setShowAlert(false) + setError(null) reset() } return ( <> - {showAlert && ( + {showAlert && error && ( (null) + const triggerRef = React.useRef(null) + const [triggerWidth, setTriggerWidth] = React.useState(220) + + React.useEffect(() => { + if (open) { + setTimeout(() => inputRef.current?.focus(), 0) + if (triggerRef.current) { + setTriggerWidth(triggerRef.current.offsetWidth) + } + } else { + setInputValue('') + } + }, [open]) const displayValue = React.useMemo(() => { if (!value) return '' @@ -54,19 +64,17 @@ export function Combobox({ }, [value]) const filteredOptions = React.useMemo(() => { - if (!inputValue) return options - - const lowerInput = inputValue.toLowerCase() - const filtered = options.filter((option) => - option.title.toLowerCase().includes(lowerInput) - ) - - // Add create option if input doesn't match any existing option - if (allowCreate && inputValue && !filtered.some((opt) => opt.title.toLowerCase() === lowerInput)) { - return [ - ...filtered, - { title: `Create new: "${inputValue}"`, inputValue } as ComboboxOption, - ] + const lowerInput = inputValue.toLowerCase().trim() + const filtered = lowerInput + ? options.filter((opt) => opt.title.toLowerCase().includes(lowerInput)) + : options + + if ( + allowCreate && + lowerInput && + !filtered.some((opt) => opt.title.toLowerCase() === lowerInput) + ) { + return [...filtered, { title: inputValue.trim(), inputValue: inputValue.trim() } as ComboboxOption] } return filtered @@ -74,80 +82,121 @@ export function Combobox({ const handleSelect = (option: ComboboxOption) => { if (option.inputValue) { - // Create new option onValueChange({ title: option.inputValue }) } else { onValueChange(option) } setOpen(false) - setInputValue('') } + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && filteredOptions.length > 0) { + e.preventDefault() + handleSelect(filteredOptions[0]) + } + if (e.key === 'Escape') { + setOpen(false) + } + } + + const isCreateOption = (option: ComboboxOption) => Boolean(option.inputValue) + const isSelected = (option: ComboboxOption) => + !isCreateOption(option) && + value && + typeof value !== 'string' && + (value as ComboboxOption).title === option.title + return (
{label && ( -
) } - From 1d5dfc2abf46df2cdb63edc5003dcecba9ddce1d Mon Sep 17 00:00:00 2001 From: motirebuma Date: Thu, 23 Apr 2026 14:03:24 -0400 Subject: [PATCH 2/5] fix for the create post and setting and privacy update user information --- .../resolvers/mutations/group/createGroup.ts | 30 ------ .../data/resolvers/mutations/post/addPost.ts | 86 ----------------- .../data/resolvers/queries/group/getGroups.ts | 15 --- quotevote-backend/app/server.ts | 95 ++++--------------- .../dashboard/settings/SettingsPageClient.tsx | 18 ++-- quotevote-frontend/src/graphql/mutations.ts | 3 - 6 files changed, 30 insertions(+), 217 deletions(-) delete mode 100644 quotevote-backend/app/data/resolvers/mutations/group/createGroup.ts delete mode 100644 quotevote-backend/app/data/resolvers/mutations/post/addPost.ts delete mode 100644 quotevote-backend/app/data/resolvers/queries/group/getGroups.ts diff --git a/quotevote-backend/app/data/resolvers/mutations/group/createGroup.ts b/quotevote-backend/app/data/resolvers/mutations/group/createGroup.ts deleted file mode 100644 index 2daf2f72..00000000 --- a/quotevote-backend/app/data/resolvers/mutations/group/createGroup.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { logger } from '~/data/utils/logger'; -import Group from '~/data/models/Group'; -import type { ResolverFn } from '~/types/graphql'; -import type * as Common from '~/types/common'; - -export const createGroup: ResolverFn = async ( - _, - args -) => { - logger.info('Function: createGroup'); - try { - const isExist = await Group.findOne({ title: args.group.title }); - if (isExist) throw new Error('Group name already exists!'); - - const newGroup = await new Group({ - ...args.group, - allowedUserIds: [args.group.creatorId], - adminIds: [args.group.creatorId], - created: new Date(), - }).save(); - - const url = `/${newGroup.title.replace(/ /g, '-').toLowerCase()}`; - await Group.findByIdAndUpdate(newGroup._id, { url }); - newGroup.url = url; - - return newGroup as unknown as Common.Group; - } catch (err) { - throw new Error(err instanceof Error ? err.message : String(err)); - } -}; diff --git a/quotevote-backend/app/data/resolvers/mutations/post/addPost.ts b/quotevote-backend/app/data/resolvers/mutations/post/addPost.ts deleted file mode 100644 index 34e0b4cc..00000000 --- a/quotevote-backend/app/data/resolvers/mutations/post/addPost.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { logger } from '~/data/utils/logger'; -import { logActivity } from '~/data/resolvers/utils/activities'; -import Group from '~/data/models/Group'; -import Post from '~/data/models/Post'; -import MessageRoom from '~/data/models/MessageRoom'; -import type { ResolverFn } from '~/types/graphql'; -import type * as Common from '~/types/common'; - -const URL_REGEX = /(?:https?:\/\/|ftp:\/\/|www\.)[^\s/$.?#].[^\s]*/gi; -const EMOJI_REGEX = - /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]/u; -const INVALID_URL_CHARS_REGEX = /[^a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=%]/; - -function sanitizeUrl(url: string): string | null { - if (!url) return null; - const trimmed = url.trim(); - if (EMOJI_REGEX.test(trimmed)) return null; - if (INVALID_URL_CHARS_REGEX.test(trimmed)) return null; - try { - const parsed = new URL(trimmed); - if (!['http:', 'https:', 'ftp:'].includes(parsed.protocol)) return null; - if (!parsed.hostname || parsed.hostname.length < 3) return null; - return parsed.href; - } catch { - return null; - } -} - -export const addPost: ResolverFn = async ( - _, - args -) => { - logger.info('Function: addPost', { args }); - - URL_REGEX.lastIndex = 0; - if (URL_REGEX.test(args.post.text)) { - logger.warn('Post rejected: URL detected in body', { text: args.post.text }); - throw new Error('Post body cannot contain links. Please use the Citation field.'); - } - - let sanitizedCitationUrl: string | null = null; - if (args.post.citationUrl) { - sanitizedCitationUrl = sanitizeUrl(args.post.citationUrl); - if (!sanitizedCitationUrl) { - logger.warn('Post rejected: Invalid citationUrl', { citationUrl: args.post.citationUrl }); - throw new Error( - 'Invalid citation URL. Only standard URL characters are allowed (no emojis or special characters).' - ); - } - } - - const group = await Group.findById(args.post.groupId); - if (!group) { - throw new Error('Group not found. Please select a valid group.'); - } - - const titleSlug = args.post.title.replace(/ /g, '-').toLowerCase(); - - try { - const newPost = await new Post({ - ...args.post, - url: '', - citationUrl: sanitizedCitationUrl, - }).save(); - - const url = `/post${group.url}/${titleSlug}/${newPost._id}`; - await Post.findByIdAndUpdate(newPost._id, { url }); - newPost.url = url; - - await MessageRoom.create({ - users: [newPost.userId], - postId: newPost._id, - messageType: 'POST', - }); - - await logActivity( - 'POSTED', - { postId: String(newPost._id), userId: String(newPost.userId) }, - newPost.title - ); - - return newPost as unknown as Common.Post; - } catch (err) { - throw new Error(err instanceof Error ? err.message : String(err)); - } -}; diff --git a/quotevote-backend/app/data/resolvers/queries/group/getGroups.ts b/quotevote-backend/app/data/resolvers/queries/group/getGroups.ts deleted file mode 100644 index 91b4dc24..00000000 --- a/quotevote-backend/app/data/resolvers/queries/group/getGroups.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Group from '~/data/models/Group'; -import type { ResolverFn } from '~/types/graphql'; -import type * as Common from '~/types/common'; - -export const getGroups: ResolverFn = async ( - _, - args -) => { - const query = Group.find({}); - if (args.limit && args.limit > 0) { - query.limit(args.limit); - } - const groups = await query.exec(); - return groups as unknown as Common.Group[]; -}; diff --git a/quotevote-backend/app/server.ts b/quotevote-backend/app/server.ts index e8e1235b..831914f8 100644 --- a/quotevote-backend/app/server.ts +++ b/quotevote-backend/app/server.ts @@ -7,9 +7,6 @@ import mongoose from 'mongoose'; import dotenv from 'dotenv'; import { GraphQLError } from 'graphql'; import { solidResolvers } from './data/resolvers/solidResolvers'; -import { addPost } from './data/resolvers/mutations/post/addPost'; -import { createGroup } from './data/resolvers/mutations/group/createGroup'; -import { getGroups } from './data/resolvers/queries/group/getGroups'; import type { GraphQLContext, PubSub } from './types/graphql'; import { requireAuth } from './data/utils/requireAuth'; import { pubsub } from './data/utils/pubsub'; @@ -47,68 +44,19 @@ async function startServer() { // 2. Apollo Server Setup (v4/v5 Syntax) const server = new ApolloServer({ typeDefs: ` - scalar JSON - scalar Date - type Query { hello: String status: String solidConnectionStatus: SolidConnectionStatus - groups(limit: Int): [Group] } type Mutation { - addPost(post: PostInput!): Post - createGroup(group: GroupInput!): Group - - solidStartConnect(issuer: String!): SolidConnectResult - solidFinishConnect(code: String!, state: String!, redirectUri: String!): SolidConnectResult - solidDisconnect: Boolean - solidPullPortableState: PortableState - solidPushPortableState(input: PortableStateInput!): Boolean - solidAppendActivityEvent(input: ActivityEventInput!): Boolean - } - - type Group { - _id: String! - creatorId: String! - adminIds: [String] - allowedUserIds: [String] - privacy: String! - title: String! - url: String - description: String - created: String - } - - type Post { - _id: String - userId: String - groupId: String - title: String - text: String - url: String - citationUrl: String - upvotes: Int - downvotes: Int - deleted: Boolean - created: String - } - - input PostInput { - userId: String! - groupId: String! - title: String! - text: String! - citationUrl: String - } - - input GroupInput { - creatorId: String! - title: String! - description: String! - privacy: String! - url: String + solidStartConnect(issuer: String!): SolidConnectResult + solidFinishConnect(code: String!, state: String!, redirectUri: String!): SolidConnectResult + solidDisconnect: Boolean + solidPullPortableState: PortableState + solidPushPortableState(input: PortableStateInput!): Boolean + solidAppendActivityEvent(input: ActivityEventInput!): Boolean } type SolidConnectionStatus { @@ -119,27 +67,29 @@ async function startServer() { } type SolidConnectResult { - authorizationUrl: String - success: Boolean - webId: String - issuer: String - message: String + authorizationUrl: String + success: Boolean + webId: String + issuer: String + message: String } + scalar JSON + type PortableState { - version: String - collections: [JSON] + version: String + collections: [JSON] } input PortableStateInput { - version: String - collections: [JSON] + version: String + collections: [JSON] } input ActivityEventInput { - type: String! - payload: JSON! - timestamp: String + type: String! + payload: JSON! + timestamp: String } `, resolvers: [ @@ -147,11 +97,6 @@ async function startServer() { Query: { hello: () => 'Hello from TypeScript Backend! 🚀', status: () => 'Active', - groups: getGroups, - }, - Mutation: { - addPost, - createGroup, }, }, solidResolvers diff --git a/quotevote-frontend/src/app/dashboard/settings/SettingsPageClient.tsx b/quotevote-frontend/src/app/dashboard/settings/SettingsPageClient.tsx index 8635ff97..5826bb46 100644 --- a/quotevote-frontend/src/app/dashboard/settings/SettingsPageClient.tsx +++ b/quotevote-frontend/src/app/dashboard/settings/SettingsPageClient.tsx @@ -100,20 +100,22 @@ export default function SettingsPageClient() { try { const result = await updateUser({ variables: { user: userInput } }) - if (result.data?.updateUser) { - const avatarValue = - typeof userData?.avatar === 'string' - ? userData.avatar - : (userData?.avatar as { url?: string } | undefined)?.url + const updated = result.data?.updateUser + if (updated) { setUserData({ ...userData, - avatar: avatarValue, - ...otherValues, + ...updated, + avatar: userData?.avatar, themePreference: localDarkMode ? 'dark' : 'light', }) setOriginalDarkMode(localDarkMode) toast.success('Settings saved successfully') - form.reset({ ...otherValues, password: '' }) + form.reset({ + name: updated.name ?? otherValues.name, + username: updated.username ?? otherValues.username, + email: updated.email ?? otherValues.email, + password: '', + }) } } catch (err) { const message = err instanceof Error ? err.message : 'Failed to save settings' diff --git a/quotevote-frontend/src/graphql/mutations.ts b/quotevote-frontend/src/graphql/mutations.ts index 9305c053..12ffdc8f 100644 --- a/quotevote-frontend/src/graphql/mutations.ts +++ b/quotevote-frontend/src/graphql/mutations.ts @@ -610,7 +610,6 @@ export const UPDATE_USER = gql` mutation updateUser($user: UserInput!) { updateUser(user: $user) { _id - id username email name @@ -618,8 +617,6 @@ export const UPDATE_USER = gql` admin accountStatus themePreference - created - updated } } ` From 2546cf298faf0868b8dd3ee8f01842910565fd9d Mon Sep 17 00:00:00 2001 From: motirebuma Date: Thu, 23 Apr 2026 14:26:43 -0400 Subject: [PATCH 3/5] ui for the notification is done --- .../NotificationsPageContent.tsx | 2 +- .../dashboard/settings/SettingsPageClient.tsx | 2 +- .../components/Notifications/Notification.tsx | 2 +- .../Notifications/NotificationLists.tsx | 16 ++++------ .../Notifications/NotificationMenu.tsx | 30 +++++++++++-------- quotevote-frontend/src/types/notification.ts | 5 +--- 6 files changed, 26 insertions(+), 31 deletions(-) diff --git a/quotevote-frontend/src/app/dashboard/notifications/NotificationsPageContent.tsx b/quotevote-frontend/src/app/dashboard/notifications/NotificationsPageContent.tsx index ecaaf679..6ccec152 100644 --- a/quotevote-frontend/src/app/dashboard/notifications/NotificationsPageContent.tsx +++ b/quotevote-frontend/src/app/dashboard/notifications/NotificationsPageContent.tsx @@ -10,7 +10,7 @@ import { Skeleton } from '@/components/ui/skeleton'; import type { Notification } from '@/types/notification'; export function NotificationsPageContent() { - const userId = useAppStore((state) => state.user.data.id); + const userId = useAppStore((state) => (state.user.data._id || state.user.data.id) as string | undefined); const { loading, data, refetch, error } = useQuery(GET_NOTIFICATIONS, { skip: !userId, diff --git a/quotevote-frontend/src/app/dashboard/settings/SettingsPageClient.tsx b/quotevote-frontend/src/app/dashboard/settings/SettingsPageClient.tsx index 5826bb46..3b86d720 100644 --- a/quotevote-frontend/src/app/dashboard/settings/SettingsPageClient.tsx +++ b/quotevote-frontend/src/app/dashboard/settings/SettingsPageClient.tsx @@ -105,7 +105,7 @@ export default function SettingsPageClient() { setUserData({ ...userData, ...updated, - avatar: userData?.avatar, + avatar: userData?.avatar as string | undefined, themePreference: localDarkMode ? 'dark' : 'light', }) setOriginalDarkMode(localDarkMode) diff --git a/quotevote-frontend/src/components/Notifications/Notification.tsx b/quotevote-frontend/src/components/Notifications/Notification.tsx index f68fb72e..3fc04d92 100644 --- a/quotevote-frontend/src/components/Notifications/Notification.tsx +++ b/quotevote-frontend/src/components/Notifications/Notification.tsx @@ -31,7 +31,7 @@ export function Notification({ if (setOpenPopUp) { setOpenPopUp(); } - router.push('/Notifications'); + router.push('/dashboard/notifications'); }; return ( diff --git a/quotevote-frontend/src/components/Notifications/NotificationLists.tsx b/quotevote-frontend/src/components/Notifications/NotificationLists.tsx index d921cfc9..9fc7efdc 100644 --- a/quotevote-frontend/src/components/Notifications/NotificationLists.tsx +++ b/quotevote-frontend/src/components/Notifications/NotificationLists.tsx @@ -5,7 +5,7 @@ import { useMutation, useApolloClient } from '@apollo/client/react'; import moment from 'moment'; import { X, UserPlus, ArrowUp, ArrowDown, MessageSquare, Quote } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import Avatar from '@/components/Avatar'; +import { DisplayAvatar } from '@/components/DisplayAvatar'; import { DELETE_NOTIFICATION } from '@/graphql/mutations'; import { GET_NOTIFICATIONS } from '@/graphql/queries'; import { useAppStore } from '@/store'; @@ -164,12 +164,6 @@ export function NotificationLists({ notifications, pageView = false }: Notificat >
{notifications.map((notification) => { - const avatarUrl = - notification.userBy.avatar?.url || - (typeof notification.userBy.avatar === 'string' - ? notification.userBy.avatar - : undefined); - const actionText = getNotificationActionText(notification.notificationType); const icon = getNotificationIcon(notification.notificationType); const displayName = notification.userBy.name || notification.userBy.username; @@ -200,10 +194,10 @@ export function NotificationLists({ notifications, pageView = false }: Notificat >
- | undefined} + username={notification.userBy.username} + size={40} /> {icon && (
diff --git a/quotevote-frontend/src/components/Notifications/NotificationMenu.tsx b/quotevote-frontend/src/components/Notifications/NotificationMenu.tsx index d513ed44..5dfea93a 100644 --- a/quotevote-frontend/src/components/Notifications/NotificationMenu.tsx +++ b/quotevote-frontend/src/components/Notifications/NotificationMenu.tsx @@ -21,7 +21,7 @@ export function NotificationMenu({ fontSize = 'small' }: NotificationMenuProps) const { isMobile } = useResponsive(); const [open, setOpen] = useState(false); const [isHovered, setIsHovered] = useState(false); - const userId = useAppStore((state) => state.user.data.id); + const userId = useAppStore((state) => (state.user.data._id || state.user.data.id) as string | undefined); const { loading, data, refetch, error } = useQuery(GET_NOTIFICATIONS, { skip: !userId, @@ -68,12 +68,14 @@ export function NotificationMenu({ fontSize = 'small' }: NotificationMenuProps) spacing={0} >
- - {notifications.length > 0 ? notifications.length : null} - + {notifications.length > 0 && ( + + {notifications.length} + + )}
- router.push('/dashboard/manage-invites')} className="cursor-pointer rounded-lg gap-3 py-2.5 px-3"> -
- -
-
-

Manage Invites

-

Invite friends to join

-
-
{isAdmin && ( router.push('/dashboard/control-panel')} className="cursor-pointer rounded-lg gap-3 py-2.5 px-3">
diff --git a/quotevote-frontend/src/app/dashboard/settings/SettingsPageClient.tsx b/quotevote-frontend/src/app/dashboard/settings/SettingsPageClient.tsx index 3b86d720..6bc9ef75 100644 --- a/quotevote-frontend/src/app/dashboard/settings/SettingsPageClient.tsx +++ b/quotevote-frontend/src/app/dashboard/settings/SettingsPageClient.tsx @@ -56,7 +56,6 @@ export default function SettingsPageClient() { const email = userData?.email ?? '' const name = userData?.name ?? '' const userId = userData?.id ?? userData?._id ?? '' - const isAdmin = Boolean(userData?.admin) const [localDarkMode, setLocalDarkMode] = useState(isDarkMode) const [originalDarkMode, setOriginalDarkMode] = useState(isDarkMode) @@ -291,16 +290,7 @@ export default function SettingsPageClient() { Sign Out - {isAdmin && ( - - )} +
- {admin && ( - - )}