diff --git a/README.md b/README.md index fe4b562..3aa3406 100644 --- a/README.md +++ b/README.md @@ -1,3 +1 @@ -[![ci-nextjs-application-template](https://github.com/ics-software-engineering/nextjs-application-template/actions/workflows/ci.yml/badge.svg)](https://github.com/ics-software-engineering/nextjs-application-template/actions/workflows/ci.yml) - -For details, please see http://ics-software-engineering.github.io/nextjs-application-template/. +# bowfolios-nextjs diff --git a/src/app/add/page.tsx b/src/app/add/page.tsx deleted file mode 100644 index fc5b82a..0000000 --- a/src/app/add/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { loggedInProtectedPage } from '@/lib/page-protection'; -import AddStuffForm from '@/components/AddStuffForm'; - -const AddStuff = async () => { - // Protect the page, only logged in users can access it. - const session = await getServerSession(authOptions); - loggedInProtectedPage( - session as { - user: { email: string; id: string; randomKey: string }; - } | null, - ); - return ( - - - - ); -}; - -export default AddStuff; diff --git a/src/app/addProject/page.tsx b/src/app/addProject/page.tsx new file mode 100644 index 0000000..7d87cd4 --- /dev/null +++ b/src/app/addProject/page.tsx @@ -0,0 +1,27 @@ +/* eslint-disable import/extensions */ +import { Container } from 'react-bootstrap'; +import { getServerSession } from 'next-auth'; +import { prisma } from '@/lib/prisma'; +import AddProjectForm from '@/components/AddProjectForm'; +import { loggedInProtectedPage } from '@/lib/page-protection'; +import { authOptions } from '../api/auth/[...nextauth]/route'; + +const AddProjectPage = async () => { + const session = await getServerSession(authOptions); + loggedInProtectedPage( + session as { + user: { email: string; id: string; randomKey: string }; + } | null + ); + const interests = await prisma.interest.findMany(); + const participants = await prisma.user.findMany(); + + return ( + + Add Project + + + ); +}; + +export default AddProjectPage; diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx deleted file mode 100644 index cd3f2ce..0000000 --- a/src/app/admin/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { getServerSession } from 'next-auth'; -import { Col, Container, Row, Table } from 'react-bootstrap'; -import StuffItemAdmin from '@/components/StuffItemAdmin'; -import { prisma } from '@/lib/prisma'; -import { adminProtectedPage } from '@/lib/page-protection'; -import { authOptions } from '../api/auth/[...nextauth]/route'; - -const AdminPage = async () => { - const session = await getServerSession(authOptions); - adminProtectedPage( - session as { - user: { email: string; id: string; randomKey: string }; - } | null, - ); - const stuff = await prisma.stuff.findMany({}); - const users = await prisma.user.findMany({}); - - return ( - - - - - List Stuff Admin - - - - Name - Quantity - Condition - Owner - Actions - - - - {stuff.map((item) => ( - - ))} - - - - - - - List Users Admin - - - - Email - Role - - - - {users.map((user) => ( - - {user.email} - {user.role} - - ))} - - - - - - - ); -}; - -export default AdminPage; diff --git a/src/app/edit/[id]/page.tsx b/src/app/edit/[id]/page.tsx deleted file mode 100644 index 3987269..0000000 --- a/src/app/edit/[id]/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { getServerSession } from 'next-auth'; -import { notFound } from 'next/navigation'; -import { Stuff } from '@prisma/client'; -import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { loggedInProtectedPage } from '@/lib/page-protection'; -import { prisma } from '@/lib/prisma'; -import EditStuffForm from '@/components/EditStuffForm'; - -export default async function EditStuffPage({ params }: { params: { id: string | string[] } }) { - // Protect the page, only logged in users can access it. - const session = await getServerSession(authOptions); - loggedInProtectedPage( - session as { - user: { email: string; id: string; randomKey: string }; - // eslint-disable-next-line @typescript-eslint/comma-dangle - } | null, - ); - const id = Number(Array.isArray(params?.id) ? params?.id[0] : params?.id); - // console.log(id); - const stuff: Stuff | null = await prisma.stuff.findUnique({ - where: { id }, - }); - // console.log(stuff); - if (!stuff) { - return notFound(); - } - - return ( - - - - ); -} diff --git a/src/app/filter/page.tsx b/src/app/filter/page.tsx new file mode 100644 index 0000000..bdbccc3 --- /dev/null +++ b/src/app/filter/page.tsx @@ -0,0 +1,33 @@ +/* eslint-disable import/extensions */ +import { getServerSession } from 'next-auth'; +import { Container } from 'react-bootstrap'; +import { prisma } from '@/lib/prisma'; +import FilterProfileForm from '@/components/FilterProfileForm'; +import { loggedInProtectedPage } from '@/lib/page-protection'; +import { authOptions } from '../api/auth/[...nextauth]/route'; + +const FilterPage = async () => { + const session = await getServerSession(authOptions); + loggedInProtectedPage( + session as { + user: { email: string; id: string; randomKey: string }; + } | null + ); + const allInterests = await prisma.interest.findMany(); + const allProfiles = await prisma.profile.findMany(); + const allProfileInterests = await prisma.profileInterest.findMany(); + const allProjects = await prisma.project.findMany(); + const allProfileProjects = await prisma.profileProject.findMany(); + return ( + + + + ); +}; +export default FilterPage; diff --git a/src/app/home/HomePage.tsx b/src/app/home/HomePage.tsx new file mode 100644 index 0000000..caaa2ff --- /dev/null +++ b/src/app/home/HomePage.tsx @@ -0,0 +1,18 @@ +'use client'; + +import React from 'react'; +import { Container, Col } from 'react-bootstrap'; +import { PageIDs } from '../../utilities/ids'; +import pageStyle from '../../utilities/pageStyle'; + +const HomePage = () => ( + + + + Your Profile + + + +); + +export default HomePage; diff --git a/src/app/home/page.tsx b/src/app/home/page.tsx new file mode 100644 index 0000000..08cd615 --- /dev/null +++ b/src/app/home/page.tsx @@ -0,0 +1,51 @@ +/* eslint-disable import/extensions */ +import React from 'react'; +import { getServerSession } from 'next-auth'; +import { prisma } from '@/lib/prisma'; +import { loggedInProtectedPage } from '@/lib/page-protection'; +import { authOptions } from '../api/auth/[...nextauth]/route'; +import HomePage from './HomePage'; + +const HomePageHelper = async () => { + const session = await getServerSession(authOptions); + // console.log(session); + loggedInProtectedPage( + session as { + user: { email: string; id: string; randomKey: string }; + } | null + ); + const email = (session && session.user && session.user.email) || ''; + const profile = await prisma.profile.findUnique({ + where: { email }, + }); + const interests = await prisma.interest.findMany(); + // const allInterestNames = interests.map((interest) => interest.name); + const projects = await prisma.project.findMany(); + // const allProjectNames = projects.map((project) => project.name); + const profileInterests = await prisma.profileInterest.findMany({ + where: { profileId: profile!.id }, + }); + const profileInterestNames = profileInterests.map((profileInterest) => { + const i = interests.find((interest) => interest.id === profileInterest.interestId); + return i ? i.name : ''; + }); + console.log(profileInterestNames); + const profileProjects = await prisma.profileProject.findMany({ + where: { profileId: profile!.id }, + }); + const profileProjectNames = profileProjects.map((profileProject) => { + const p = projects.find((project) => project.id === profileProject.projectId); + return p ? p.name : ''; + }); + console.log( + // interests, + // projects, + // profileInterests, + // profileProjects, + profileInterestNames, + profileProjectNames + ); + return ; +}; + +export default HomePageHelper; diff --git a/src/app/interests/InterestCard.tsx b/src/app/interests/InterestCard.tsx new file mode 100644 index 0000000..72eadc6 --- /dev/null +++ b/src/app/interests/InterestCard.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { Card } from 'react-bootstrap'; +// eslint-disable-next-line import/extensions +import TooltipImage from '@/components/TooltipImage'; +import { InterestCardData } from './InterestCardData'; + +const InterestCard = ({ interest }: { interest: InterestCardData }) => ( + + + {interest.name} + {interest.profilePictures.map((p) => ( + + ))} + {interest.projectPictures.map((p) => ( + + ))} + + +); +export default InterestCard; diff --git a/src/app/interests/InterestCardData.ts b/src/app/interests/InterestCardData.ts new file mode 100644 index 0000000..fcd8157 --- /dev/null +++ b/src/app/interests/InterestCardData.ts @@ -0,0 +1,10 @@ +export type PictureInfo = { + name: string; + picture: string | null; +}; + +export type InterestCardData = { + name: string; + profilePictures: (PictureInfo | null)[]; + projectPictures: (PictureInfo | null)[]; +}; diff --git a/src/app/interests/InterestCardHelper.tsx b/src/app/interests/InterestCardHelper.tsx new file mode 100644 index 0000000..7e44d2d --- /dev/null +++ b/src/app/interests/InterestCardHelper.tsx @@ -0,0 +1,34 @@ +import { Interest } from '@prisma/client'; +// eslint-disable-next-line import/extensions +import { prisma } from '@/lib/prisma'; +import InterestCard from './InterestCard'; + +const InterestCardHelper = async ({ interest }: { interest: Interest }) => { + const profileInterests = await prisma.profileInterest.findMany({ + where: { interestId: interest.id }, + }); + // console.log('profileInterests: ', profileInterests); + const profiles = await prisma.profile.findMany({ + where: { id: { in: profileInterests.map((profileInterest) => profileInterest.profileId) } }, + }); + // console.log('profiles: ', profiles); + const profileImages = profiles.map((profile) => ({ name: profile.email, picture: profile.picture })); + // console.log('profileImages: ', profileImages); + const projectInterests = await prisma.projectInterest.findMany({ + where: { interestId: interest.id }, + }); + const projects = await prisma.project.findMany({ + where: { id: { in: projectInterests.map((projectInterest) => projectInterest.projectId) } }, + }); + // console.log('projects: ', projects); + const projectImages = projects.map((project) => ({ name: project.name, picture: project.picture })); + // console.log('projectImages: ', projectImages); + const interestData = { + name: interest.name, + profilePictures: profileImages, + projectPictures: projectImages, + }; + return ; +}; + +export default InterestCardHelper; diff --git a/src/app/interests/page.tsx b/src/app/interests/page.tsx new file mode 100644 index 0000000..6bebbb3 --- /dev/null +++ b/src/app/interests/page.tsx @@ -0,0 +1,20 @@ +import { prisma } from '@/lib/prisma'; +import { Container, Row } from 'react-bootstrap'; +import { PageIDs } from '@/utilities/ids'; +import { pageStyle } from '@/utilities/pageStyle'; +import InterestCardHelper from './InterestCardHelper'; + +const InterestsPage = async () => { + const interests = await prisma.interest.findMany(); + interests.sort((a, b) => a.name.localeCompare(b.name)); + console.log(interests); + return ( + + + {interests.map((interest) => ())} + + + ); +}; + +export default InterestsPage; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d6e4002..8fcdf68 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -9,7 +9,7 @@ import Providers from './providers'; const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { - title: 'Next.js Application Template', + title: 'Bowfolios', description: 'Generated by ics-software-engineering.github.io', }; diff --git a/src/app/lib/ProfileCardData.ts b/src/app/lib/ProfileCardData.ts new file mode 100644 index 0000000..e366b5c --- /dev/null +++ b/src/app/lib/ProfileCardData.ts @@ -0,0 +1,12 @@ +import { Project } from '@prisma/client'; + +export type ProfileCardData = { + email: string; + bio: string | null; + firstName: string | null; + lastName: string | null; + picture: string | null; + title: string | null; + projects: Project[]; + interests: string[]; +}; diff --git a/src/app/lib/ProjectCardData.ts b/src/app/lib/ProjectCardData.ts new file mode 100644 index 0000000..3481820 --- /dev/null +++ b/src/app/lib/ProjectCardData.ts @@ -0,0 +1,10 @@ +import { Profile } from '@prisma/client'; + +export type ProjectCardData = { + name: string; + homepage: string | null; + picture: string | null; + description: string | null; + interests: string[]; + participants: Profile[]; +}; diff --git a/src/app/lib/StickyState.ts b/src/app/lib/StickyState.ts new file mode 100644 index 0000000..4e2fa30 --- /dev/null +++ b/src/app/lib/StickyState.ts @@ -0,0 +1,16 @@ +import { Entity, entity } from 'simpler-state'; + +const entities: Record; savedSetter: (value: any) => void }> = {}; + +// eslint-disable-next-line import/prefer-default-export +export const useStickyState = (name: string, initialValue: any) => { + if (entities[name]) { + const { savedEntity, savedSetter } = entities[name]; + return [savedEntity.use(), savedSetter]; + } + // Otherwise create a new entity + const newEntity = entity(initialValue); + const newEntitySetter = (newValue: unknown) => newEntity.set(newValue); + entities[name] = { savedEntity: newEntity, savedSetter: newEntitySetter }; + return [newEntity.use(), newEntitySetter]; +}; diff --git a/src/app/lib/dbActions.ts b/src/app/lib/dbActions.ts new file mode 100644 index 0000000..5e2044d --- /dev/null +++ b/src/app/lib/dbActions.ts @@ -0,0 +1,100 @@ +'use server'; + +import { compare, hash } from 'bcrypt'; +import { prisma } from './prisma'; + +export async function getUser(email: string) { + // console.log(`getUser data: ${email}`); + // eslint-disable-next-line @typescript-eslint/return-await + return await prisma.user.findUnique({ + where: { email }, + }); +} + +export async function checkPassword(credentials: { email: string; password: string }) { + // console.log(`checkPassword data: ${JSON.stringify(credentials, null, 2)}`); + const user = await getUser(credentials.email); + if (!user) { + return false; + } + // eslint-disable-next-line @typescript-eslint/return-await + return await compare(credentials.password, user.password); +} + +export async function changePassword(credentials: { email: string; password: string }) { + // console.log(`changePassword data: ${JSON.stringify(credentials, null, 2)}`); + const password = await hash(credentials.password, 10); + await prisma.user.update({ + where: { email: credentials.email }, + data: { + password, + }, + }); +} + +export async function createUser(credentials: { email: string; password: string }) { + // console.log(`createUser data: ${JSON.stringify(credentials, null, 2)}`); + const password = await hash(credentials.password, 10); + await prisma.user.create({ + data: { + email: credentials.email, + password, + }, + }); +} + +export async function createProject(project: any) { + // console.log(`createProject data: ${JSON.stringify(project, null, 2)}`); + const dbProject = await prisma.project.create({ + data: project, + }); + return dbProject; +} + +export async function upsertProject(project: any) { + // console.log(`upsertProject data: ${JSON.stringify(project, null, 2)}`); + const dbProject = await prisma.project.upsert({ + where: { name: project.name }, + update: {}, + create: { + name: project.name, + description: project.description, + homepage: project.homepage, + picture: project.picture, + }, + }); + project.interests.forEach(async (intere: string) => { + const dbInterest = await prisma.interest.findUnique({ + where: { name: intere }, + }); + // console.log(`${dbProject.name} ${dbInterest!.name}`); + const dbProjectInterest = await prisma.projectInterest.findMany({ + where: { projectId: dbProject.id, interestId: dbInterest!.id }, + }); + if (dbProjectInterest.length === 0) { + await prisma.projectInterest.create({ + data: { + projectId: dbProject.id, + interestId: dbInterest!.id, + }, + }); + } + }); + project.participants.forEach(async (email: string) => { + const dbProfile = await prisma.profile.findUnique({ + where: { email }, + }); + const dbProfileProject = await prisma.profileProject.findMany({ + where: { projectId: dbProject.id, profileId: dbProfile!.id }, + }); + if (dbProfileProject.length === 0) { + await prisma.profileProject.create({ + data: { + projectId: dbProject.id, + profileId: dbProfile!.id, + }, + }); + } + }); + return dbProject; +} diff --git a/src/app/lib/page-protection.ts b/src/app/lib/page-protection.ts new file mode 100644 index 0000000..0d157e9 --- /dev/null +++ b/src/app/lib/page-protection.ts @@ -0,0 +1,15 @@ +import { redirect } from 'next/navigation'; +import { Role } from '@prisma/client'; + +export const loggedInProtectedPage = (session: { user: { email: string; id: string; randomKey: string } } | null) => { + if (!session) { + redirect('/auth/signin'); + } +}; + +export const adminProtectedPage = (session: { user: { email: string; id: string; randomKey: string } } | null) => { + loggedInProtectedPage(session); + if (session && session.user.randomKey !== Role.ADMIN) { + redirect('/not-authorized'); + } +}; diff --git a/src/app/lib/prisma.ts b/src/app/lib/prisma.ts new file mode 100644 index 0000000..966ee7f --- /dev/null +++ b/src/app/lib/prisma.ts @@ -0,0 +1,19 @@ +import { PrismaClient } from '@prisma/client'; + +// PrismaClient is attached to the `global` object in development to prevent +// exhausting your database connection limit. +// +// Learn more: +// https://pris.ly/d/help/next-js-best-practices + +const globalForPrisma = global as unknown as { prisma: PrismaClient }; + +// eslint-disable-next-line import/prefer-default-export, operator-linebreak +export const prisma = + // eslint-disable-next-line operator-linebreak + globalForPrisma.prisma || + new PrismaClient({ + log: ['query'], // CAM: is this the right level of logging? + }); + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; diff --git a/src/app/lib/validationSchemas.ts b/src/app/lib/validationSchemas.ts new file mode 100644 index 0000000..a104c66 --- /dev/null +++ b/src/app/lib/validationSchemas.ts @@ -0,0 +1,21 @@ +import * as Yup from 'yup'; + +export interface IProject { + name: string; + homepage?: string; + picture?: string; + description: string; + interests?: (string | undefined)[] | undefined; + participants?: (string | undefined)[] | undefined; +} + +export const AddProjectSchema = Yup.object().shape({ + name: Yup.string().required('Name is required'), + homepage: Yup.string().optional().url('Homepage must be a valid URL'), + picture: Yup.string().optional(), + description: Yup.string().required('Description is required'), + interests: Yup.array().of(Yup.string()), + participants: Yup.array().of(Yup.string()), +}); + +export default AddProjectSchema; diff --git a/src/app/list/page.tsx b/src/app/list/page.tsx deleted file mode 100644 index 399106e..0000000 --- a/src/app/list/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { getServerSession } from 'next-auth'; -import { Col, Container, Row, Table } from 'react-bootstrap'; -import { prisma } from '@/lib/prisma'; -import StuffItem from '@/components/StuffItem'; -import { loggedInProtectedPage } from '@/lib/page-protection'; -import { authOptions } from '@/app/api/auth/[...nextauth]/route'; - -/** Render a list of stuff for the logged in user. */ -const ListPage = async () => { - // Protect the page, only logged in users can access it. - const session = await getServerSession(authOptions); - loggedInProtectedPage( - session as { - user: { email: string; id: string; randomKey: string }; - // eslint-disable-next-line @typescript-eslint/comma-dangle - } | null, - ); - const owner = (session && session.user && session.user.email) || ''; - const stuff = await prisma.stuff.findMany({ - where: { - owner, - }, - }); - // console.log(stuff); - return ( - - - - - Stuff - - - - Name - Quantity - Condition - Actions - - - - {stuff.map((item) => ( - - ))} - - - - - - - ); -}; - -export default ListPage; diff --git a/src/app/profiles/ProfileCardHelper.tsx b/src/app/profiles/ProfileCardHelper.tsx new file mode 100644 index 0000000..96e0d74 --- /dev/null +++ b/src/app/profiles/ProfileCardHelper.tsx @@ -0,0 +1,35 @@ +/* eslint-disable import/extensions */ +import { Profile } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; +import { ProfileCardData } from '@/lib/ProfileCardData'; +import ProfileCard from '../../components/ProfileCard'; + +const ProfileCardHelper = async ({ profile }: { profile: Profile }) => { + const profileInterests = await prisma.profileInterest.findMany({ + where: { profileId: profile.id }, + }); + const interests = await prisma.interest.findMany({ + where: { id: { in: profileInterests.map((profileInterest) => profileInterest.interestId) } }, + }); + const interestNames = interests.map((interest) => interest.name); + const profileProjects = await prisma.profileProject.findMany({ + where: { profileId: profile.id }, + }); + console.log('profileProjects: ', profileProjects); + const projects = await prisma.project.findMany({ + where: { id: { in: profileProjects.map((profileProject) => profileProject.projectId) } }, + }); + const profileData: ProfileCardData = { + firstName: profile.firstName, + lastName: profile.lastName, + email: profile.email, + bio: profile.bio, + title: profile.title, + picture: profile.picture, + interests: interestNames, + projects, + }; + return ; +}; + +export default ProfileCardHelper; diff --git a/src/app/profiles/page.tsx b/src/app/profiles/page.tsx new file mode 100644 index 0000000..bca0a05 --- /dev/null +++ b/src/app/profiles/page.tsx @@ -0,0 +1,22 @@ +/* eslint-disable import/extensions */ +import { Container, Row } from 'react-bootstrap'; +import { prisma } from '@/lib/prisma'; +import { PageIDs } from '@/utilities/ids'; +import pageStyle from '@/utilities/pageStyle'; +import ProfileCardHelper from './ProfileCardHelper'; + +const ProfilesPage = async () => { + const profiles = await prisma.profile.findMany(); + profiles.sort((a, b) => a.email.localeCompare(b.email)); + return ( + + + {profiles.map((profile) => ( + + ))} + + + ); +}; + +export default ProfilesPage; diff --git a/src/app/projects/ProjectCardHelper.tsx b/src/app/projects/ProjectCardHelper.tsx new file mode 100644 index 0000000..1a3c1da --- /dev/null +++ b/src/app/projects/ProjectCardHelper.tsx @@ -0,0 +1,33 @@ +/* eslint-disable import/extensions */ +import { Project } from '@prisma/client'; +import ProjectCard from '@/components/ProjectCard'; +import { prisma } from '@/lib/prisma'; +import { ProjectCardData } from '@/lib/ProjectCardData'; + +const ProjectCardHelper = async ({ project }: { project: Project }) => { + const projectInterests = await prisma.projectInterest.findMany({ + where: { projectId: project.id }, + }); + const interests = await prisma.interest.findMany({ + where: { id: { in: projectInterests.map((projectInterest) => projectInterest.interestId) } }, + }); + const interestNames = interests.map((interest) => interest.name); + const projectParticipants = await prisma.profileProject.findMany({ + where: { projectId: project.id }, + }); + const participants = projectParticipants.map((projectParticipant) => projectParticipant.profileId); + const profileParticipants = await prisma.profile.findMany({ + where: { id: { in: participants } }, + }); + const projectData: ProjectCardData = { + name: project.name, + homepage: project.homepage, + picture: project.picture, + description: project.description, + interests: interestNames, + participants: profileParticipants, + }; + return ; +}; + +export default ProjectCardHelper; diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx new file mode 100644 index 0000000..8145d4f --- /dev/null +++ b/src/app/projects/page.tsx @@ -0,0 +1,22 @@ +/* eslint-disable import/extensions */ +import { Container, Row } from 'react-bootstrap'; +import { prisma } from '@/lib/prisma'; +import { PageIDs } from '@/utilities/ids'; +import pageStyle from '@/utilities/pageStyle'; +import ProjectCardHelper from './ProjectCardHelper'; + +const ProjectsPage = async () => { + const projects = await prisma.project.findMany(); + projects.sort((a, b) => a.name.localeCompare(b.name)); + return ( + + + {projects.map((project) => ( + + ))} + + + ); +}; + +export default ProjectsPage; diff --git a/src/app/utilities/ids.ts b/src/app/utilities/ids.ts new file mode 100644 index 0000000..cb767c5 --- /dev/null +++ b/src/app/utilities/ids.ts @@ -0,0 +1,48 @@ +export const ComponentIDs = { + addProjectMenuItem: 'addProjectMenuItem', + addProjectFormName: 'addProjectFormName', + addProjectFormPicture: 'addProjectFormPicture', + addProjectFormHomePage: 'addProjectFormHomePage', + addProjectFormDescription: 'addProjectFormDescription', + addProjectFormInterests: 'addProjectFormInterests', + addProjectFormParticipants: 'addProjectFormParticipants', + addProjectFormSubmit: 'addProjectFormSubmit', + basicNavbarNav: 'basicNavbarNav', + currentUserDropdown: 'currentUserDropdown', + currentUserDropdownSignOut: 'currentUserDropdownSignOut', + filterFormInterests: 'filterFormInterests', + filterFormSubmit: 'filterFormSubmit', + filterMenuItem: 'filterMenuItem', + homeFormFirstName: 'homeFormFirstName', + homeFormLastName: 'homeFormLastName', + homeFormBio: 'homeFormBio', + homeFormSubmit: 'homeFormSubmit', + homeMenuItem: 'homeMenuItem', + interestsMenuItem: 'interestsMenuItem', + loginDropdown: 'loginDropdown', + loginDropdownSignIn: 'loginDropdownSignIn', + loginDropdownSignUp: 'loginDropdownSignUp', + profilesMenuItem: 'profilesMenuItem', + projectsMenuItem: 'projectsMenuItem', + signInFormEmail: 'signInFormEmail', + signInFormPassword: 'signInFormPassword', + signInFormSubmit: 'signInFormSubmit', + signUpFormEmail: 'signUpFormEmail', + signUpFormPassword: 'signUpFormPassword', + signUpFormSubmit: 'signUpFormSubmit', +}; + +export const PageIDs = { + addProjectPage: 'addProjectPage', + filterPage: 'filterPage', + homePage: 'homePage', + interestsPage: 'interestsPage', + landingPage: 'landingPage', + notAuthorizedPage: 'notAuthorizedPage', + notFoundPage: 'notFoundPage', + profilesPage: 'profilesPage', + projectsPage: 'projectsPage', + signInPage: 'signInPage', + signOutPage: 'signOutPage', + signUpPage: 'signUpPage', +}; diff --git a/src/app/utilities/pageStyle.ts b/src/app/utilities/pageStyle.ts new file mode 100644 index 0000000..2ff3983 --- /dev/null +++ b/src/app/utilities/pageStyle.ts @@ -0,0 +1,6 @@ +const pageStyle = { + paddingTop: 10, + paddingBottom: 20, +}; + +export default pageStyle; diff --git a/src/components/AddProjectForm.tsx b/src/components/AddProjectForm.tsx new file mode 100644 index 0000000..426f00e --- /dev/null +++ b/src/components/AddProjectForm.tsx @@ -0,0 +1,128 @@ +/* eslint-disable import/extensions */ + +'use client'; + +import React from 'react'; +import { Form, Button, Col, Container, Card, Row } from 'react-bootstrap'; +import { useForm, Controller } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import swal from 'sweetalert'; +import Multiselect from 'multiselect-react-dropdown'; +import { Interest, User } from '@prisma/client'; +import { AddProjectSchema, IProject } from '@/lib/validationSchemas'; +import { upsertProject } from '@/lib/dbActions'; + +const AddProjectForm = ({ interests, participants }: { interests: Interest[]; participants: User[] }) => { + const formPadding = 'py-1'; + const interestNames = interests.map((interest) => interest.name); + const participantNames = participants.map((participant) => participant.email); + const { + register, + handleSubmit, + control, + reset, + formState: { errors }, + } = useForm({ + resolver: yupResolver(AddProjectSchema), + }); + + const onSubmit = async (data: IProject) => { + const result = await upsertProject(data); + if (result) { + swal('Success!', 'Project data saved successfully!', 'success'); + reset(); + } else { + swal('Error!', 'Failed to save project data!', 'error'); + } + }; + + return ( + + + + + + + + Name + + {errors.name?.message} + + + + + Picture + + {errors.picture?.message} + + + + + Homepage + + {errors.homepage?.message} + + + + + + Description + + (optional) + + + + + + Interests + ( + + )} + /> + + + + + Participants + ( + + )} + /> + + + + + Submit + + + + + + ); +}; + +export default AddProjectForm; diff --git a/src/components/AddStuffForm.tsx b/src/components/AddStuffForm.tsx deleted file mode 100644 index e1397ef..0000000 --- a/src/components/AddStuffForm.tsx +++ /dev/null @@ -1,102 +0,0 @@ -'use client'; - -import { useSession } from 'next-auth/react'; -import { Button, Card, Col, Container, Form, Row } from 'react-bootstrap'; -import { useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -import swal from 'sweetalert'; -import { redirect } from 'next/navigation'; -import { addStuff } from '@/lib/dbActions'; -import LoadingSpinner from '@/components/LoadingSpinner'; -import { AddStuffSchema } from '@/lib/validationSchemas'; - -const onSubmit = async (data: { name: string; quantity: number; owner: string; condition: string }) => { - // console.log(`onSubmit data: ${JSON.stringify(data, null, 2)}`); - await addStuff(data); - swal('Success', 'Your item has been added', 'success', { - timer: 2000, - }); -}; - -const AddStuffForm: React.FC = () => { - const { data: session, status } = useSession(); - // console.log('AddStuffForm', status, session); - const currentUser = session?.user?.email || ''; - const { - register, - handleSubmit, - reset, - formState: { errors }, - } = useForm({ - resolver: yupResolver(AddStuffSchema), - }); - if (status === 'loading') { - return ; - } - if (status === 'unauthenticated') { - redirect('/auth/signin'); - } - - return ( - - - - - Add Stuff - - - - - - Name - - {errors.name?.message} - - - Quantity - - {errors.quantity?.message} - - - Condition - - Excellent - Good - Fair - Poor - - {errors.condition?.message} - - - - - - - Submit - - - - reset()} variant="warning" className="float-right"> - Reset - - - - - - - - - - - ); -}; - -export default AddStuffForm; diff --git a/src/components/EditStuffForm.tsx b/src/components/EditStuffForm.tsx deleted file mode 100644 index 3a4a58d..0000000 --- a/src/components/EditStuffForm.tsx +++ /dev/null @@ -1,100 +0,0 @@ -'use client'; - -import { Button, Card, Col, Container, Form, Row } from 'react-bootstrap'; -import { useForm } from 'react-hook-form'; -import swal from 'sweetalert'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { Stuff } from '@prisma/client'; -import { EditStuffSchema } from '@/lib/validationSchemas'; -import { editStuff } from '@/lib/dbActions'; - -const onSubmit = async (data: Stuff) => { - // console.log(`onSubmit data: ${JSON.stringify(data, null, 2)}`); - await editStuff(data); - swal('Success', 'Your item has been updated', 'success', { - timer: 2000, - }); -}; - -const EditStuffForm = ({ stuff }: { stuff: Stuff }) => { - const { - register, - handleSubmit, - reset, - formState: { errors }, - } = useForm({ - resolver: yupResolver(EditStuffSchema), - }); - // console.log(stuff); - - return ( - - - - - Edit Stuff - - - - - - - Name - - {errors.name?.message} - - - Quantity - - {errors.quantity?.message} - - - Condition - - Excellent - Good - Fair - Poor - - {errors.condition?.message} - - - - - - - Submit - - - - reset()} variant="warning" className="float-right"> - Reset - - - - - - - - - - - ); -}; - -export default EditStuffForm; diff --git a/src/components/FilterProfileForm.tsx b/src/components/FilterProfileForm.tsx new file mode 100644 index 0000000..f63b0b8 --- /dev/null +++ b/src/components/FilterProfileForm.tsx @@ -0,0 +1,117 @@ +/* eslint-disable import/extensions */ + +'use client'; + +import React from 'react'; +import { Form, Button, Card, Container, Row } from 'react-bootstrap'; +import { useForm, Controller } from 'react-hook-form'; +import Multiselect from 'multiselect-react-dropdown'; +import { Interest, Profile, ProfileInterest, ProfileProject, Project } from '@prisma/client'; +import { useStickyState } from '@/lib/StickyState'; +import { ProfileCardData } from '@/lib/ProfileCardData'; +import ProfileCard from './ProfileCard'; + +const FilterProfileForm = ({ + interests, + profiles, + profileInterests, + profileProjects, + projects, +}: { + interests: Interest[]; + profiles: Profile[]; + profileInterests: ProfileInterest[]; + profileProjects: ProfileProject[]; + projects: Project[]; +}) => { + const [selectedInterests, setSelectedInterests] = useStickyState('selectedInterests', []); + const interestNames = interests.map((interest) => interest.name); + const selectedInterestIds = interests + .filter((interest) => selectedInterests.includes(interest.name)) + .map((interest) => interest.id); + const filteredProfileIds = profileInterests + .filter((profileInterest) => selectedInterestIds.includes(profileInterest.interestId)) + .map((profileInterest) => profileInterest.profileId); + const filteredProfiles = profiles.filter((profile) => filteredProfileIds.includes(profile.id)); + + const { handleSubmit, control } = useForm({}); + + const onSubmit = (data: { [x: string]: any }) => { + setSelectedInterests(data.interests); + }; + + const getProfileInterestNames = (profileId: number): string[] => { + const profileInterestIds = profileInterests + .filter((profileInterest) => profileInterest.profileId === profileId) + .map((profileInterest) => profileInterest.interestId); + const profileInterestNames = interests + .filter((interest) => profileInterestIds.includes(interest.id)) + .map((interest) => interest.name); + return profileInterestNames; + }; + + const getProfileProjects = (profileId: number): Project[] => { + const profileProjectIds = profileProjects + .filter((profileProject) => profileProject.profileId === profileId) + .map((profileProject) => profileProject.projectId); + const filteredProjects = projects.filter((project) => profileProjectIds.includes(project.id)); + return filteredProjects; + }; + + const makeProfileData = (profile: Profile): ProfileCardData => { + const profileInterestNames = getProfileInterestNames(profile.id); + const profProjects = getProfileProjects(profile.id); + const retVal = { + email: profile.email, + bio: profile.bio, + firstName: profile.firstName, + lastName: profile.lastName, + picture: profile.picture, + title: profile.title, + projects: profProjects, + interests: profileInterestNames, + }; + return retVal; + }; + + return ( + + + + + + Interests + ( + + )} + /> + + + + Submit + + + + + + {filteredProfiles.map((profile) => ( + + ))} + + + ); +}; + +export default FilterProfileForm; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index e0848f4..91de3ea 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,17 +1,19 @@ import { Col, Container } from 'react-bootstrap'; -/** The Footer appears at the bottom of every page. Rendered by the App Layout component. */ +/* The Footer appears at the bottom of every page. Rendered by the App Layout component. */ const Footer = () => ( -