diff --git a/src/app/solo-project/[id]/page.tsx b/src/app/solo-project/[id]/page.tsx index 7cd114a..ba1e0a5 100644 --- a/src/app/solo-project/[id]/page.tsx +++ b/src/app/solo-project/[id]/page.tsx @@ -1,11 +1,18 @@ -import {getSoloProjectById, setEvaluatorOnDb, updateSoloProjectById} from "@/services/soloProjects"; +import { + getAllSoloProjectsByUser, + getSoloProjectById, + setEvaluatorOnDb, + updateSoloProjectById +} from "@/services/soloProjects"; import ProjectSubmissionDetail from "@/components/soloprojects/Details"; import {ActionResponse} from "@/types"; import Comments from "@/components/comments"; import FeedbackContainer from "@/components/feedback/FeedbackContainer"; +import CompactList from "@/components/soloprojects/CompactList"; const SoloProjectPage = async ({params}: { params: { id: string } }) => { const record = await getSoloProjectById(params.id) + const projects = await getAllSoloProjectsByUser(record.fields["Discord ID"], record.fields.Email) const handleSave = async (evalFeedback: string, evalStatus: string): Promise => { 'use server' return await updateSoloProjectById(params.id, { @@ -20,6 +27,7 @@ const SoloProjectPage = async ({params}: { params: { id: string } }) => { return(
+ {projects.length>1 && } { + return( + + + {records.map(record=>( + + + {record.fields.Timestamp.toString()} + {record.fields["Evaluation Status"]} + + + ))} + +
+ ) + } + + export default CompactList \ No newline at end of file diff --git a/src/components/soloprojects/Details.tsx b/src/components/soloprojects/Details.tsx index 0637191..5e3e322 100644 --- a/src/components/soloprojects/Details.tsx +++ b/src/components/soloprojects/Details.tsx @@ -15,6 +15,7 @@ import {ActionResponse} from "@/types"; import {CopyToClipboard} from "react-copy-to-clipboard"; import {toast} from "react-hot-toast"; import {getRandomPassMessage} from "@/lib/getRandomPassMessage"; +import {parseRole} from "@/lib/parseRole"; interface ProjectDetailProps { record: Submission, @@ -119,8 +120,8 @@ const ProjectSubmissionDetail = ( {record.fields["Voyage Role (from Applications link)"] ?
- {record.fields["Voyage Role (from Applications link)"]} + className={`text-center ${roleColors[parseRole(record.fields["Voyage Role (from Applications link)"])]?.bg} py-1 mt-3`}> + {parseRole(record.fields["Voyage Role (from Applications link)"])}
:
No Role Selected
} diff --git a/src/lib/airtable.ts b/src/lib/airtable.ts index 3ff5ccf..dc3bc94 100644 --- a/src/lib/airtable.ts +++ b/src/lib/airtable.ts @@ -1,79 +1,64 @@ -import Airtable, {FieldSet, Record, Records} from "airtable"; -import {EvaluationStatus, Submission, VoyageRole} from "@/types/SoloProjectTypes"; - -const base = new Airtable({apiKey: process.env.AIRTABLE_PAT}) - .base(process.env.AIRTABLE_BASEID as string) - -const table = base(process.env.AIRTABLE_TABLEID as string) -const userTable = base(process.env.AIRTABLE_USERS_TABLEID as string) - -const fields = [ - "Discord Name", - "GitHub ID", - "Timestamp", - "Tier", - "GitHub Repo URL", - "Deployed App URL", - "Evaluation Status", - "Evaluator", - "Evaluation Feedback", - "Instructions", - "Addl. Comments", - "Voyage Role (from Applications link)", - "Discord ID" -] - -const transformData = (records:Records
): Submission[] => { - return records.map((record: Record
)=>{ - return { - id: record.id, - commentCount: record.commentCount as number, - fields: { - "Discord Name": record.fields["Discord Name"] as string, - "GitHub ID": record.fields["GitHub ID"] as string, - "Timestamp": record.fields["Timestamp"] as string, - "Tier": record.fields["Tier"] as string, - "GitHub Repo URL": record.fields["GitHub Repo URL"] as string, - "Deployed App URL": record.fields["Deployed App URL"] as string, - "Evaluation Status": record.fields["Evaluation Status"] as EvaluationStatus, - "Evaluator": record.fields["Evaluator"] as string, - "Evaluation Feedback": record.fields["Evaluation Feedback"] as string, - "Instructions": record.fields["Instructions"] as string, - "Addl. Comments": record.fields["Addl. Comments"] as string, - "Voyage Role (from Applications link)": record.fields["Voyage Role (from Applications link)"] as VoyageRole, - "Discord ID": record.fields["Discord ID"] as string - } - } - }) -} - -// TODO: might be able to refactor the following duplicated code -const transformDataSingleRecord = (record:Record
) => { - return { - id: record.id, - commentCount: record.commentCount as number, - fields: { - "Discord Name": record.fields["Discord Name"] as string, - "GitHub ID": record.fields["GitHub ID"] as string, - "Timestamp": record.fields["Timestamp"] as string, - "Tier": record.fields["Tier"] as string, - "GitHub Repo URL": record.fields["GitHub Repo URL"] as string, - "Deployed App URL": record.fields["Deployed App URL"] as string, - "Evaluation Status": record.fields["Evaluation Status"] as EvaluationStatus, - "Evaluator": record.fields["Evaluator"] as string, - "Evaluation Feedback": record.fields["Evaluation Feedback"] as string, - "Instructions": record.fields["Instructions"] as string, - "Addl. Comments": record.fields["Addl. Comments"] as string, - "Voyage Role (from Applications link)": record.fields["Voyage Role (from Applications link)"] as VoyageRole, - "Discord ID": record.fields["Discord ID"] as string - } - } -} - -export { - table, - userTable, - fields, - transformData, - transformDataSingleRecord +import Airtable, {FieldSet, Record, Records} from "airtable"; +import {EvaluationStatus, Submission, VoyageRole} from "@/types/SoloProjectTypes"; + +const base = new Airtable({apiKey: process.env.AIRTABLE_PAT}) + .base(process.env.AIRTABLE_BASEID as string) + +const table = base(process.env.AIRTABLE_TABLEID as string) +const userTable = base(process.env.AIRTABLE_USERS_TABLEID as string) + +const fields = [ + "Email", + "Discord Name", + "GitHub ID", + "Timestamp", + "Tier", + "GitHub Repo URL", + "Deployed App URL", + "Evaluation Status", + "Evaluator", + "Evaluation Feedback", + "Instructions", + "Addl. Comments", + "Voyage Role (from Applications link)", + "Discord ID" +] + +const transformRecord = (record: Record
) => { + return { + id: record.id, + commentCount: record.commentCount as number, + fields: { + "Email": record.fields["Email"] as string, + "Discord Name": record.fields["Discord Name"] as string, + "GitHub ID": record.fields["GitHub ID"] as string, + "Timestamp": record.fields["Timestamp"] as string, + "Tier": record.fields["Tier"] as string, + "GitHub Repo URL": record.fields["GitHub Repo URL"] as string, + "Deployed App URL": record.fields["Deployed App URL"] as string, + "Evaluation Status": record.fields["Evaluation Status"] as EvaluationStatus, + "Evaluator": record.fields["Evaluator"] as string, + "Evaluation Feedback": record.fields["Evaluation Feedback"] as string, + "Instructions": record.fields["Instructions"] as string, + "Addl. Comments": record.fields["Addl. Comments"] as string, + "Voyage Role (from Applications link)": record.fields["Voyage Role (from Applications link)"] as VoyageRole, + "Discord ID": record.fields["Discord ID"] as string + } + } +} + +const transformData = (records:Records
): Submission[] => { + return records.map((record: Record
)=>transformRecord(record)) +} + +const transformDataSingleRecord = (record:Record
) => { + return transformRecord(record) +} + +export { + table, + userTable, + fields, + transformData, + transformDataSingleRecord } \ No newline at end of file diff --git a/src/lib/parseRole.ts b/src/lib/parseRole.ts new file mode 100644 index 0000000..030a96b --- /dev/null +++ b/src/lib/parseRole.ts @@ -0,0 +1,11 @@ +// Sometimes on airtable, VoyageRole would be something like ["Software Developer", "Software Developer"] +// due to members having multiple applications +// IF the roles are the same, it will just return one - for `roleColors` +// Otherwise, it will return "unknown" +import {VoyageRole} from "@/types/SoloProjectTypes"; + +export const parseRole = (role: VoyageRole): VoyageRole => { + const set = new Set(role) + if (set.size === 1) return [...set].toString() as VoyageRole + return "unknown" +} \ No newline at end of file diff --git a/src/services/soloProjects.ts b/src/services/soloProjects.ts index 5c1bbeb..91bc3e6 100644 --- a/src/services/soloProjects.ts +++ b/src/services/soloProjects.ts @@ -1,95 +1,104 @@ -"use server" -import {fields, table, transformData, transformDataSingleRecord} from "@/lib/airtable"; -import {Submission} from "@/types/SoloProjectTypes"; -import {ActionResponse} from "@/types"; -import {getServerSession} from "next-auth"; -import {options} from "@/app/api/auth/[...nextauth]/options"; -import AirtableError from "airtable/lib/airtable_error"; -import {FieldSet} from "airtable"; -import {getDate} from "@/lib/getDate"; - -export const getAllSoloProjects = async (): Promise => { - const records = await table.select({}).firstPage() - return transformData(records) -} - -export const getSoloProjectsByStatus = async (status:string): Promise => { - const filter = `{Evaluation Status} = "${status}"` - const records = await table.select({ - filterByFormula: filter, - fields: fields, - recordMetadata: ["commentCount"], - sort:[{ - field:"Timestamp", - direction: "desc" - }] - }).firstPage() - return transformData(records) -} - -export const getSoloProjectById = async (id: string): Promise => { - const record = await table.find(id) - return transformDataSingleRecord(record) -} - -export const setEvaluatorOnDb = async (id: string): Promise => { - const sessionData = await getServerSession(options) - try{ - const record = await table.find(id) - if(record.fields["Evaluator"]){ - return { - success: false, - message: `${record.fields["Evaluator"]} is already evaluating this solo project submission. ` - } - }else{ - if(sessionData){ - const updatedRecord = await table.update([ - { - id, - fields: { - "Evaluation Date": getDate(), - "Evaluator": sessionData.user.evaluatorEmail - } - } - ]) - return { - success: true, - message: `Evaluator is set to ${updatedRecord[0].fields["Evaluator"]}.`, - data: transformData(updatedRecord)[0] - } - } - return { - success: false, - message: `You're not logged in.` - } - } - }catch (e){ - if (e instanceof AirtableError){ - if (e.error === 'INVALID_MULTIPLE_CHOICE_OPTIONS') - return { - success: false, - message: `Airtable error: Your email ${sessionData?.user.evaluatorEmail} has not been added to the evaluator list. Please contact an administrator.` - } - } - return { - success: false, - message: `Error: ${e}` - } - } - -} - -export const updateSoloProjectById = async (id: string, fields: FieldSet) - :Promise => { - const updatedRecord = await table.update([ - { - id, - fields - } - ]) - return { - success: true, - message: `update success`, - data: transformData(updatedRecord)[0] - } +"use server" +import {fields, table, transformData, transformDataSingleRecord} from "@/lib/airtable"; +import {Submission} from "@/types/SoloProjectTypes"; +import {ActionResponse} from "@/types"; +import {getServerSession} from "next-auth"; +import {options} from "@/app/api/auth/[...nextauth]/options"; +import AirtableError from "airtable/lib/airtable_error"; +import {FieldSet} from "airtable"; +import {getDate} from "@/lib/getDate"; + +export const getAllSoloProjects = async (): Promise => { + const records = await table.select({}).firstPage() + return transformData(records) +} + +export const getSoloProjectsByStatus = async (status:string): Promise => { + const filter = `{Evaluation Status} = "${status}"` + const records = await table.select({ + filterByFormula: filter, + fields: fields, + recordMetadata: ["commentCount"], + sort:[{ + field:"Timestamp", + direction: "desc" + }] + }).firstPage() + return transformData(records) +} + +export const getSoloProjectById = async (id: string): Promise => { + const record = await table.find(id) + return transformDataSingleRecord(record) +} + +export const getAllSoloProjectsByUser = async (discordId: string, email: string): Promise => { + const filter = `OR({Discord ID} = "${discordId}", Email = "${email}")` + const records = await table.select({ + filterByFormula: filter, + fields, + }).all() + return transformData(records) +} + +export const setEvaluatorOnDb = async (id: string): Promise => { + const sessionData = await getServerSession(options) + try{ + const record = await table.find(id) + if(record.fields["Evaluator"]){ + return { + success: false, + message: `${record.fields["Evaluator"]} is already evaluating this solo project submission. ` + } + }else{ + if(sessionData){ + const updatedRecord = await table.update([ + { + id, + fields: { + "Evaluation Date": getDate(), + "Evaluator": sessionData.user.evaluatorEmail + } + } + ]) + return { + success: true, + message: `Evaluator is set to ${updatedRecord[0].fields["Evaluator"]}.`, + data: transformData(updatedRecord)[0] + } + } + return { + success: false, + message: `You're not logged in.` + } + } + }catch (e){ + if (e instanceof AirtableError){ + if (e.error === 'INVALID_MULTIPLE_CHOICE_OPTIONS') + return { + success: false, + message: `Airtable error: Your email ${sessionData?.user.evaluatorEmail} has not been added to the evaluator list. Please contact an administrator.` + } + } + return { + success: false, + message: `Error: ${e}` + } + } + +} + +export const updateSoloProjectById = async (id: string, fields: FieldSet) + :Promise => { + const updatedRecord = await table.update([ + { + id, + fields + } + ]) + return { + success: true, + message: `update success`, + data: transformData(updatedRecord)[0] + } } \ No newline at end of file diff --git a/src/styles/roles.ts b/src/styles/roles.ts index 78750cd..27b5ce9 100644 --- a/src/styles/roles.ts +++ b/src/styles/roles.ts @@ -1,18 +1,22 @@ -export const roleColors = { - "Software Developer": { - text: 'text-blue-400', - bg: 'bg-blue-400 text-black' - }, - "UI / UX Designer": { - text: 'text-teal-200', - bg: 'bg-teal-200 text-black' - }, - "Data Scientist": { - text: 'text-amber-200', - bg: 'bg-amber-200 text-black' - }, - "Product Owner": { - text: 'text-red-900', - bg: 'bg-red-900 text-red-50' - }, +export const roleColors = { + "Software Developer": { + text: 'text-blue-400', + bg: 'bg-blue-400 text-black' + }, + "UI / UX Designer": { + text: 'text-teal-200', + bg: 'bg-teal-200 text-black' + }, + "Data Scientist": { + text: 'text-amber-200', + bg: 'bg-amber-200 text-black' + }, + "Product Owner": { + text: 'text-red-900', + bg: 'bg-red-900 text-red-50' + }, + "unknown": { + text: 'text-gray-500', + bg: 'text-gray-500 text-black' + } } \ No newline at end of file diff --git a/src/types/SoloProjectTypes.ts b/src/types/SoloProjectTypes.ts index d664600..931d472 100644 --- a/src/types/SoloProjectTypes.ts +++ b/src/types/SoloProjectTypes.ts @@ -1,36 +1,38 @@ -export type Submission = { - id: string - commentCount: number - fields: FilteredFields -} - -export type EvaluationStatus = - | "Waiting Eval" - | "Passed" - | "Not in Discord" - | "Requested Changes" - | "Pending" - | "No Pass" - -export type VoyageRole = - | "Software Developer" - | "UI / UX Designer" - | "Data Scientist" - | "Product Owner" - -export type FilteredFields = { - "Discord Name": string - "GitHub ID": string - "Timestamp": Date | string - "Tier": string - "GitHub Repo URL": string - "Deployed App URL": string - "Evaluation Status":EvaluationStatus - "Evaluator": string - "Evaluation Feedback": string - "Instructions": string - "Addl. Comments": string - "Voyage Role (from Applications link)": VoyageRole - "Discord ID": string -} - +export type Submission = { + id: string + commentCount: number + fields: FilteredFields +} + +export type EvaluationStatus = + | "Waiting Eval" + | "Passed" + | "Not in Discord" + | "Requested Changes" + | "Pending" + | "No Pass" + +export type VoyageRole = + | "Software Developer" + | "UI / UX Designer" + | "Data Scientist" + | "Product Owner" + | "unknown" + +export type FilteredFields = { + "Email": string + "Discord Name": string + "GitHub ID": string + "Timestamp": Date | string + "Tier": string + "GitHub Repo URL": string + "Deployed App URL": string + "Evaluation Status":EvaluationStatus + "Evaluator": string + "Evaluation Feedback": string + "Instructions": string + "Addl. Comments": string + "Voyage Role (from Applications link)": VoyageRole + "Discord ID": string +} +