From 0af67819bbd54ffe3c444ff061c8b0733fcc85d6 Mon Sep 17 00:00:00 2001 From: cuteolaf Date: Thu, 16 Mar 2023 19:00:43 -0700 Subject: [PATCH 1/6] fix route: /briefs/api --- api/src/backend/models.ts | 955 +++++++++++++++++++++----------------- 1 file changed, 523 insertions(+), 432 deletions(-) diff --git a/api/src/backend/models.ts b/api/src/backend/models.ts index 73e66ea4..941a9390 100644 --- a/api/src/backend/models.ts +++ b/api/src/backend/models.ts @@ -1,35 +1,35 @@ import { knex, Knex } from "knex"; import db from "./db/index"; -import { StreamChat } from 'stream-chat'; +import { StreamChat } from "stream-chat"; export type FederatedCredential = { - id: number, - issuer: string, - subject: string, + id: number; + issuer: string; + subject: string; }; export type Skill = { - id: number, - name: string -} + id: number; + name: string; +}; export type Industry = { - id: number, - name: string -} + id: number; + name: string; +}; export type Language = { - id: number, - name: string -} + id: number; + name: string; +}; export type Service = { - id: number, - name: string -} + id: number; + name: string; +}; export type Web3Account = { - address: string, + address: string; user_id: number; type: string; challenge: string; @@ -76,7 +76,7 @@ export type MilestoneDetails = { index: number | string; project_id: number | string; details: string; -} +}; export type Project = { id?: string | number; @@ -91,8 +91,8 @@ export type Project = { owner?: string; user_id?: string | number; brief_id?: string | number; - total_cost_without_fee?: number; - imbue_fee?: number; + total_cost_without_fee?: number; + imbue_fee?: number; }; export type ProjectProperties = { @@ -117,8 +117,8 @@ export type Brief = { duration_id: number; duration: string; budget: bigint; - experience_level: string, - experience_id: number + experience_level: string; + experience_id: number; user_id: number; }; @@ -147,7 +147,6 @@ export type Freelancer = { num_ratings: number; }; - export type BriefSqlFilter = { experience_range: number[]; submitted_range: number[]; @@ -164,154 +163,183 @@ export type FreelancerSqlFilter = { search_input: string; }; -export const fetchWeb3Account = (address: string) => - (tx: Knex.Transaction) => - tx("web3_accounts") - .select() - .where({ address, }) - .first(); +export const fetchWeb3Account = (address: string) => (tx: Knex.Transaction) => + tx("web3_accounts").select().where({ address }).first(); -export const fetchUser = (id: number) => - (tx: Knex.Transaction) => - tx("users").where({ id }).first(); +export const fetchUser = (id: number) => (tx: Knex.Transaction) => + tx("users").where({ id }).first(); -export const fetchUserOrEmail = (userOrEmail: string) => - (tx: Knex.Transaction) => - tx("users").where({ username: userOrEmail }) +export const fetchUserOrEmail = + (userOrEmail: string) => (tx: Knex.Transaction) => + tx("users") + .where({ username: userOrEmail }) .orWhere({ email: userOrEmail.toLowerCase() }) .first() .debug(true); -export const upsertWeb3Challenge = ( - user: User, - address: string, - type: string, - challenge: string, -) => async (tx: Knex.Transaction): - Promise<[web3Account: Web3Account, isInsert: boolean]> => { - +export const upsertWeb3Challenge = + (user: User, address: string, type: string, challenge: string) => + async ( + tx: Knex.Transaction + ): Promise<[web3Account: Web3Account, isInsert: boolean]> => { const web3Account = await tx("web3_accounts") .select() .where({ - user_id: user?.id + user_id: user?.id, }) .first(); if (!web3Account) { return [ ( - await tx("web3_accounts").insert({ - address, - user_id: user.id, - type, - challenge, - }).returning("*") + await tx("web3_accounts") + .insert({ + address, + user_id: user.id, + type, + challenge, + }) + .returning("*") )[0], - true + true, ]; } return [ ( - await tx("web3_accounts").update({ challenge }).where( - { user_id: user.id } - ).returning("*") + await tx("web3_accounts") + .update({ challenge }) + .where({ user_id: user.id }) + .returning("*") )[0], - false + false, ]; }; -export const insertUserByDisplayName = (displayName: string, username: string) => - async (tx: Knex.Transaction) => ( - await tx("users").insert({ - display_name: displayName, - username: username, - }).returning("*") - )[0]; +export const insertUserByDisplayName = + (displayName: string, username: string) => async (tx: Knex.Transaction) => + ( + await tx("users") + .insert({ + display_name: displayName, + username: username, + }) + .returning("*") + )[0]; export const generateGetStreamToken = async (user: User) => { - if (process.env.REACT_APP_GETSTREAM_API_KEY && process.env.REACT_APP_GETSTREAM_SECRET_KEY) { - const client: StreamChat = new StreamChat(process.env.REACT_APP_GETSTREAM_API_KEY, process.env.REACT_APP_GETSTREAM_SECRET_KEY); + if ( + process.env.REACT_APP_GETSTREAM_API_KEY && + process.env.REACT_APP_GETSTREAM_SECRET_KEY + ) { + const client: StreamChat = new StreamChat( + process.env.REACT_APP_GETSTREAM_API_KEY, + process.env.REACT_APP_GETSTREAM_SECRET_KEY + ); const token = client.createToken(user.username); - await client.upsertUser({ id: user.username}); + await client.upsertUser({ id: user.username }); return token; } - return "" -} - -export const updateUserGetStreamToken = (id: number, token: string ) => - async (tx: Knex.Transaction) => ( - await tx("users").where({id}).update({ - getstream_token: token - }).returning("*") - )[0]; - -export const insertToTable = (item: string, table_name: string) => - async (tx: Knex.Transaction) => ( - await tx(table_name).insert({ - name: item.toLowerCase() - }).returning("*") - )[0]; - -export const updateFederatedLoginUser = (user: User, username: string, email: string, password: string) => - async (tx: Knex.Transaction) => ( - await tx("users").update({ - username: username.toLowerCase(), - email: email.toLowerCase(), - password: password - }).where({ - id: user.id - }).returning("*") - )[0]; - -export const insertProject = (project: Project) => - async (tx: Knex.Transaction) => ( - await tx("projects").insert(project).returning("*") - )[0]; - -export const updateProject = (id: string | number, project: Project) => - async (tx: Knex.Transaction) => ( - await tx("projects") - .update(project) - .where({ id }) - .returning("*") - )[0]; - -export const updateProjectProperties = (id: string | number, properties: ProjectProperties) => - async (tx: Knex.Transaction) => ( - await tx("project_properties") - .update(properties) - .where({ 'project_id': id }) - .returning("*") - )[0]; - - -export const fetchUserBriefApplications = (user_id: string | number, brief_id: string | number) => -(tx: Knex.Transaction) => - tx("projects").select().where({ user_id, brief_id }).first(); - -export const fetchProject = (id: string | number) => - (tx: Knex.Transaction) => - tx("projects").select().where({ id }).first(); - + return ""; +}; -export const fetchProjectWithProperties = (id: string | number) => +export const updateUserGetStreamToken = + (id: number, token: string) => async (tx: Knex.Transaction) => + ( + await tx("users") + .where({ id }) + .update({ + getstream_token: token, + }) + .returning("*") + )[0]; + +export const insertToTable = + (item: string, table_name: string) => + async (tx: Knex.Transaction) => + ( + await tx(table_name) + .insert({ + name: item.toLowerCase(), + }) + .returning("*") + )[0]; + +export const updateFederatedLoginUser = + (user: User, username: string, email: string, password: string) => + async (tx: Knex.Transaction) => + ( + await tx("users") + .update({ + username: username.toLowerCase(), + email: email.toLowerCase(), + password: password, + }) + .where({ + id: user.id, + }) + .returning("*") + )[0]; + +export const insertProject = + (project: Project) => async (tx: Knex.Transaction) => + (await tx("projects").insert(project).returning("*"))[0]; + +export const updateProject = + (id: string | number, project: Project) => async (tx: Knex.Transaction) => + ( + await tx("projects") + .update(project) + .where({ id }) + .returning("*") + )[0]; + +export const updateProjectProperties = + (id: string | number, properties: ProjectProperties) => + async (tx: Knex.Transaction) => + ( + await tx("project_properties") + .update(properties) + .where({ project_id: id }) + .returning("*") + )[0]; + +export const fetchUserBriefApplications = + (user_id: string | number, brief_id: string | number) => (tx: Knex.Transaction) => - tx("projects").join("project_properties", "projects.id", "=", "project_properties.project_id").select().where({ "project_id": id }).first(); + tx("projects").select().where({ user_id, brief_id }).first(); + +export const fetchProject = (id: string | number) => (tx: Knex.Transaction) => + tx("projects").select().where({ id }).first(); + +export const fetchProjectWithProperties = + (id: string | number) => (tx: Knex.Transaction) => + tx("projects") + .join( + "project_properties", + "projects.id", + "=", + "project_properties.project_id" + ) + .select() + .where({ project_id: id }) + .first(); -export const fetchAllProjects = () => - (tx: Knex.Transaction) => - tx("projects").select(); +export const fetchAllProjects = () => (tx: Knex.Transaction) => + tx("projects").select(); -export const fetchUserProject = (id: string | number) => - (tx: Knex.Transaction) => - tx("projects").select().where({ - user_id: id - }).first(); +export const fetchUserProject = + (id: string | number) => (tx: Knex.Transaction) => + tx("projects") + .select() + .where({ + user_id: id, + }) + .first(); export const insertMilestones = ( milestones: ProposedMilestone[], - project_id: string | number, + project_id: string | number ) => { const values = milestones.map((m, idx) => ({ ...m, @@ -323,62 +351,63 @@ export const insertMilestones = ( tx("milestones").insert(values).returning("*"); }; -export const deleteMilestones = (project_id: string | number) => - (tx: Knex.Transaction) => +export const deleteMilestones = + (project_id: string | number) => (tx: Knex.Transaction) => tx("milestones").delete().where({ project_id }); -export const fetchProjectMilestones = (id: string | number) => - (tx: Knex.Transaction) => +export const fetchProjectMilestones = + (id: string | number) => (tx: Knex.Transaction) => tx("milestones").select().where({ project_id: id }); -export const updateMilestoneDetails = (id: string | number, milestoneId: string | number, details: string) => (tx: Knex.Transaction) => - tx("milestone_details").where({ project_id: id }).where('index', '=', milestoneId).update('details', details).returning("*"); - -export const insertMilestoneDetails = (value: MilestoneDetails) => async (tx: Knex.Transaction) => (await - tx("milestone_details").insert(value).returning("*"))[0]; - -export const fetchAllMilestone = (id: string | number) => +export const updateMilestoneDetails = + (id: string | number, milestoneId: string | number, details: string) => (tx: Knex.Transaction) => - tx("milestone_details").where('project_id', '=', id); - -export const fetchMilestoneByIndex = (projectId: string | number, milestoneId: string | number) => - (tx: Knex.Transaction) => - tx("milestone_details").select().where({ project_id: projectId }).where('index', '=', milestoneId); - -export const fetchBriefApplications = (id: string) => + tx("milestone_details") + .where({ project_id: id }) + .where("index", "=", milestoneId) + .update("details", details) + .returning("*"); + +export const insertMilestoneDetails = + (value: MilestoneDetails) => async (tx: Knex.Transaction) => + ( + await tx("milestone_details") + .insert(value) + .returning("*") + )[0]; + +export const fetchAllMilestone = + (id: string | number) => (tx: Knex.Transaction) => + tx("milestone_details").where("project_id", "=", id); + +export const fetchMilestoneByIndex = + (projectId: string | number, milestoneId: string | number) => (tx: Knex.Transaction) => - fetchAllProjects()(tx) - .where({ "brief_id": id }) + tx("milestone_details") .select() + .where({ project_id: projectId }) + .where("index", "=", milestoneId); -export const fetchBriefProjects = (brief_id: string) => - (tx: Knex.Transaction) => - fetchAllProjects()(tx) - .where({ brief_id }) - .select() +export const fetchBriefApplications = (id: string) => (tx: Knex.Transaction) => + fetchAllProjects()(tx).where({ brief_id: id }).select(); -export const fetchAcceptedBriefs = (user_id: string) => - (tx: Knex.Transaction) => - fetchAllBriefs()(tx) - .where({ user_id }) - .select() +export const fetchBriefProjects = + (brief_id: string) => (tx: Knex.Transaction) => + fetchAllProjects()(tx).where({ brief_id }).select(); -export const fetchBrief = (id: string) => - (tx: Knex.Transaction) => - fetchAllBriefs()(tx) - .where({ "briefs.id": id }) - .first() +export const fetchAcceptedBriefs = + (user_id: string) => (tx: Knex.Transaction) => + fetchAllBriefs()(tx).where({ user_id }).select(); +export const fetchBrief = (id: string) => (tx: Knex.Transaction) => + fetchAllBriefs()(tx).where({ "briefs.id": id }).first(); -export const fetchUserBriefs = (user_id: string) => - (tx: Knex.Transaction) => - fetchAllBriefs()(tx) - .where({ user_id }) - .select() +export const fetchUserBriefs = (user_id: string) => (tx: Knex.Transaction) => + fetchAllBriefs()(tx).where({ user_id }).select(); -export const fetchAllBriefs = () => - (tx: Knex.Transaction) => - tx.select( +export const fetchAllBriefs = () => (tx: Knex.Transaction) => + tx + .select( "briefs.id", "headline", "description", @@ -392,119 +421,136 @@ export const fetchAllBriefs = () => "briefs.experience_id", "briefs.created", "briefs.user_id", - "briefs.project_id", "users.briefs_submitted as number_of_briefs_submitted", tx.raw("ARRAY_AGG(DISTINCT CAST(skills.name as text)) as skills"), tx.raw("ARRAY_AGG(DISTINCT CAST(skills.id as text)) as skill_ids"), - tx.raw("ARRAY_AGG(DISTINCT CAST(industries.name as text)) as industries"), - tx.raw("ARRAY_AGG(DISTINCT CAST(industries.id as text)) as industry_ids"), + tx.raw( + "ARRAY_AGG(DISTINCT CAST(industries.name as text)) as industries" + ), + tx.raw( + "ARRAY_AGG(DISTINCT CAST(industries.id as text)) as industry_ids" + ) ) - .from("briefs") - .leftJoin("brief_industries", { "briefs.id": "brief_industries.brief_id" }) - .leftJoin("industries", { "brief_industries.industry_id": "industries.id" }) - .leftJoin("brief_skills", { "briefs.id": "brief_skills.brief_id" }) - .leftJoin("skills", { "brief_skills.skill_id": "skills.id" }) - .leftJoin("experience", { 'briefs.experience_id': "experience.id" }) - .leftJoin("scope", { "briefs.scope_id": "scope.id" }) - .leftJoin("duration", { "briefs.duration_id": "duration.id" }) - .innerJoin("users", { "briefs.user_id": "users.id" }) - .orderBy("briefs.created", "desc") - .groupBy("briefs.id") - .groupBy("scope.scope_level") - .groupBy("duration.duration") - .groupBy("users.display_name") - .groupBy("briefs.experience_id") - .groupBy("experience.experience_level") - .groupBy("users.id") - -export const fetchItems = (ids: number[], tableName: string) => - async (tx: Knex.Transaction) => - tx(tableName).select("id", "name") - .whereIn(`id`, ids); + .from("briefs") + .leftJoin("brief_industries", { + "briefs.id": "brief_industries.brief_id", + }) + .leftJoin("industries", { + "brief_industries.industry_id": "industries.id", + }) + .leftJoin("brief_skills", { "briefs.id": "brief_skills.brief_id" }) + .leftJoin("skills", { "brief_skills.skill_id": "skills.id" }) + .leftJoin("experience", { "briefs.experience_id": "experience.id" }) + .leftJoin("scope", { "briefs.scope_id": "scope.id" }) + .leftJoin("duration", { "briefs.duration_id": "duration.id" }) + .innerJoin("users", { "briefs.user_id": "users.id" }) + .orderBy("briefs.created", "desc") + .groupBy("briefs.id") + .groupBy("scope.scope_level") + .groupBy("duration.duration") + .groupBy("users.display_name") + .groupBy("briefs.experience_id") + .groupBy("experience.experience_level") + .groupBy("users.id"); + +export const fetchItems = + (ids: number[], tableName: string) => async (tx: Knex.Transaction) => + tx(tableName).select("id", "name").whereIn(`id`, ids); // export const fetchSkills = (ids: string[]) => // (tx: Knex.Transaction) => // tx("skills").select("id","name").whereIn('id', ids ); // Insert a brief and their respective skill and industry_ids. -export const insertBrief = (brief: Brief, skill_ids: number[], industry_ids: number[], scope_id: number, duration_id: number) => - async (tx: Knex.Transaction) => ( - await tx("briefs").insert({ - headline: brief.headline, - description: brief.description, - duration_id: duration_id, - scope_id: scope_id, - user_id: brief.user_id, - budget: brief.budget, - experience_id: brief.experience_id, - }).returning("briefs.id") +export const insertBrief = + ( + brief: Brief, + skill_ids: number[], + industry_ids: number[], + scope_id: number, + duration_id: number + ) => + async (tx: Knex.Transaction) => + await tx("briefs") + .insert({ + headline: brief.headline, + description: brief.description, + duration_id: duration_id, + scope_id: scope_id, + user_id: brief.user_id, + budget: brief.budget, + experience_id: brief.experience_id, + }) + .returning("briefs.id") .then(async (ids) => { if (skill_ids) { skill_ids.forEach(async (skillId) => { if (skillId) { - await tx("brief_skills") - .insert({ - brief_id: ids[0], - skill_id: skillId - }) + await tx("brief_skills").insert({ + brief_id: ids[0], + skill_id: skillId, + }); } - - }) + }); } if (industry_ids) { industry_ids.forEach(async (industry_id) => { if (industry_id) { - await tx("brief_industries") - .insert({ - brief_id: ids[0], - industry_id: industry_id - }) + await tx("brief_industries").insert({ + brief_id: ids[0], + industry_id: industry_id, + }); } + }); + } + return ids[0]; + }); +export const incrementUserBriefSubmissions = + (id: number) => async (tx: Knex.Transaction) => + tx("users").where({ id: id }).increment("briefs_submitted", 1); + +export const insertFederatedCredential = + (id: number, issuer: string, subject: string) => + async (tx: Knex.Transaction) => + ( + await tx("federated_credentials") + .insert({ + id, + issuer, + subject, + }) + .returning("*") + )[0]; + +export const upsertItems = + (items: string[], tableName: string) => async (tx: Knex.Transaction) => { + var item_ids: number[] = []; + try { + //TODO Convert to map + for (const item of items) { + var item_id: number; + const existing_item = await tx(tableName) + .select() + .where({ + name: item.toLowerCase(), }) - } - return ids[0] - }) - ); + .first(); -export const incrementUserBriefSubmissions = (id: number) => - async (tx: Knex.Transaction) => ( - tx("users").where({ id: id }).increment('briefs_submitted', 1) - ); + if (!existing_item) { + item_id = await ( + await insertToTable(item, tableName)(tx) + ).id; + } else item_id = existing_item.id; -export const insertFederatedCredential = ( - id: number, - issuer: string, - subject: string, -) => async (tx: Knex.Transaction) => ( - await tx("federated_credentials").insert({ - id, issuer, subject - }).returning("*") -)[0]; - -export const upsertItems = (items: string[], tableName: string) => async (tx: Knex.Transaction) => { - var item_ids: number[] = []; - try { - //TODO Convert to map - for (const item of items) { - var item_id: number; - const existing_item = await tx(tableName).select().where({ - name: item.toLowerCase() - }).first(); - - if (!existing_item) { - item_id = await (await insertToTable(item, tableName)(tx)).id; - } else - item_id = existing_item.id - - item_ids.push(item_id); + item_ids.push(item_id); + } + } catch (err) { + console.log("Failed to insert new item ", err); } - } catch (err) { - console.log("Failed to insert new item ", err) - } - return item_ids; -}; + return item_ids; + }; export const getOrCreateFederatedUser = ( issuer: string, @@ -512,19 +558,22 @@ export const getOrCreateFederatedUser = ( displayName: string, done: CallableFunction ) => { - db.transaction(async tx => { + db.transaction(async (tx) => { let user: User; - - try { /** * Do we already have a federated_credential ? */ - const federated = await tx("federated_credentials").select().where({ - issuer, - subject: username, - }).first(); + const federated = await tx( + "federated_credentials" + ) + .select() + .where({ + issuer, + subject: username, + }) + .first(); /** * If not, create the `usr`, then the `federated_credential` @@ -533,52 +582,48 @@ export const getOrCreateFederatedUser = ( user = await insertUserByDisplayName(displayName, username)(tx); await insertFederatedCredential(user.id, issuer, username)(tx); } else { - const user_ = await db.select().from("users").where({ - id: federated.id - }).first(); + const user_ = await db + .select() + .from("users") + .where({ + id: federated.id, + }) + .first(); if (!user_) { throw new Error( - `Unable to find matching user by \`federated_credential.id\`: ${federated.id - }` + `Unable to find matching user by \`federated_credential.id\`: ${federated.id}` ); } user = user_; } - if(!user.getstream_token) { + if (!user.getstream_token) { const token = await generateGetStreamToken(user); await updateUserGetStreamToken(user.id, token)(tx); } done(null, user); } catch (err) { - done(new Error( - "Failed to upsert federated authentication." - )); + done(new Error("Failed to upsert federated authentication.")); } }); }; -export const fetchFreelancerDetailsByUserID = (user_id: number | string) => - (tx: Knex.Transaction) => - fetchAllFreelancers()(tx) - .where({ user_id }) - .first() - .debug(false) +export const fetchFreelancerDetailsByUserID = + (user_id: number | string) => (tx: Knex.Transaction) => + fetchAllFreelancers()(tx).where({ user_id }).first().debug(false); -export const fetchFreelancerDetailsByUsername = (username: string) => - (tx: Knex.Transaction) => +export const fetchFreelancerDetailsByUsername = + (username: string) => (tx: Knex.Transaction) => fetchAllFreelancers()(tx) .where({ username: username }) .first() - .debug(false) - + .debug(false); -export const fetchAllFreelancers = () => - (tx: Knex.Transaction) => - - tx.select( +export const fetchAllFreelancers = () => (tx: Knex.Transaction) => + tx + .select( "freelancers.id", "freelanced_before", "freelancing_goal", @@ -598,53 +643,86 @@ export const fetchAllFreelancers = () => tx.raw("ARRAY_AGG(DISTINCT CAST(skills.name as text)) as skills"), tx.raw("ARRAY_AGG(DISTINCT CAST(skills.id as text)) as skill_ids"), - tx.raw("ARRAY_AGG(DISTINCT CAST(languages.name as text)) as languages"), - tx.raw("ARRAY_AGG(DISTINCT CAST(languages.id as text)) as language_ids"), + tx.raw( + "ARRAY_AGG(DISTINCT CAST(languages.name as text)) as languages" + ), + tx.raw( + "ARRAY_AGG(DISTINCT CAST(languages.id as text)) as language_ids" + ), - tx.raw("ARRAY_AGG(DISTINCT CAST(services.name as text)) as services"), - tx.raw("ARRAY_AGG(DISTINCT CAST(services.id as text)) as service_ids"), + tx.raw( + "ARRAY_AGG(DISTINCT CAST(services.name as text)) as services" + ), + tx.raw( + "ARRAY_AGG(DISTINCT CAST(services.id as text)) as service_ids" + ), tx.raw("ARRAY_AGG(DISTINCT CAST(clients.name as text)) as clients"), - tx.raw("ARRAY_AGG(DISTINCT CAST(clients.id as text)) as client_ids"), - - tx.raw("ARRAY_AGG(DISTINCT CAST(clients.img as text)) as client_images"), - tx.raw("ARRAY_AGG(DISTINCT CAST(clients.id as text)) as client_image_ids"), - tx.raw("(SUM(freelancer_ratings.rating) / COUNT(freelancer_ratings.rating)) as rating"), - tx.raw("COUNT(freelancer_ratings.rating) as num_ratings"), - - ).from("freelancers") - // Join services and many to many - .leftJoin("freelancer_services", { 'freelancers.id': "freelancer_services.freelancer_id" }) - .leftJoin("services", { 'freelancer_services.service_id': "services.id" }) - // Join clients and many to many - .leftJoin("freelancer_clients", { 'freelancers.id': "freelancer_clients.freelancer_id" }) - .leftJoin("clients", { 'freelancer_clients.client_id': "clients.id" }) - // Join skills and many to many - .leftJoin("freelancer_skills", { 'freelancers.id': "freelancer_skills.freelancer_id" }) - .leftJoin("skills", { 'freelancer_skills.skill_id': "skills.id" }) - // Join languages and many to many - .leftJoin("freelancer_languages", { 'freelancers.id': "freelancer_languages.freelancer_id" }) - .leftJoin("languages", { 'freelancer_languages.language_id': "languages.id" }) - .innerJoin("users", { "freelancers.user_id": "users.id" }) - .leftJoin("freelancer_ratings", { "freelancers.id": "freelancer_ratings.freelancer_id" }) - - // order and group by many-many selects - .orderBy("freelancers.created", "desc") - .groupBy("freelancers.id") - .groupBy("users.username") - .groupBy("users.display_name") - .orderBy("freelancers.id", "desc") - // TODO Add limit until we have spinning loading icon in freelancers page - .limit(100) - - -export const insertFreelancerDetails = ( - f: Freelancer, skill_ids: number[], - language_ids: number[], client_ids: number[], - service_ids: number[]) => + tx.raw( + "ARRAY_AGG(DISTINCT CAST(clients.id as text)) as client_ids" + ), + + tx.raw( + "ARRAY_AGG(DISTINCT CAST(clients.img as text)) as client_images" + ), + tx.raw( + "ARRAY_AGG(DISTINCT CAST(clients.id as text)) as client_image_ids" + ), + tx.raw( + "(SUM(freelancer_ratings.rating) / COUNT(freelancer_ratings.rating)) as rating" + ), + tx.raw("COUNT(freelancer_ratings.rating) as num_ratings") + ) + .from("freelancers") + // Join services and many to many + .leftJoin("freelancer_services", { + "freelancers.id": "freelancer_services.freelancer_id", + }) + .leftJoin("services", { + "freelancer_services.service_id": "services.id", + }) + // Join clients and many to many + .leftJoin("freelancer_clients", { + "freelancers.id": "freelancer_clients.freelancer_id", + }) + .leftJoin("clients", { "freelancer_clients.client_id": "clients.id" }) + // Join skills and many to many + .leftJoin("freelancer_skills", { + "freelancers.id": "freelancer_skills.freelancer_id", + }) + .leftJoin("skills", { "freelancer_skills.skill_id": "skills.id" }) + // Join languages and many to many + .leftJoin("freelancer_languages", { + "freelancers.id": "freelancer_languages.freelancer_id", + }) + .leftJoin("languages", { + "freelancer_languages.language_id": "languages.id", + }) + .innerJoin("users", { "freelancers.user_id": "users.id" }) + .leftJoin("freelancer_ratings", { + "freelancers.id": "freelancer_ratings.freelancer_id", + }) + + // order and group by many-many selects + .orderBy("freelancers.created", "desc") + .groupBy("freelancers.id") + .groupBy("users.username") + .groupBy("users.display_name") + .orderBy("freelancers.id", "desc") + // TODO Add limit until we have spinning loading icon in freelancers page + .limit(100); + +export const insertFreelancerDetails = + ( + f: Freelancer, + skill_ids: number[], + language_ids: number[], + client_ids: number[], + service_ids: number[] + ) => async (tx: Knex.Transaction) => - await tx("freelancers").insert( - { + await tx("freelancers") + .insert({ freelanced_before: f.freelanced_before.toString(), freelancing_goal: f.freelancing_goal, work_type: f.work_type, @@ -656,135 +734,148 @@ export const insertFreelancerDetails = ( twitter_link: f.twitter_link, telegram_link: f.telegram_link, discord_link: f.discord_link, - user_id: f.user_id + user_id: f.user_id, }) .returning("id") - .then(ids => { + .then((ids) => { if (skill_ids) { skill_ids.forEach(async (skillId) => { if (skillId) { - await tx("freelancer_skills") - .insert({ - freelancer_id: ids[0], - skill_id: skillId - }) + await tx("freelancer_skills").insert({ + freelancer_id: ids[0], + skill_id: skillId, + }); } - - }) + }); } if (language_ids) { language_ids.forEach(async (langId) => { if (langId) { - await tx("freelancer_languages") - .insert({ - freelancer_id: ids[0], - language_id: langId - }) + await tx("freelancer_languages").insert({ + freelancer_id: ids[0], + language_id: langId, + }); } - }) + }); } if (client_ids) { client_ids.forEach(async (clientId) => { if (clientId) { - await tx("freelancer_clients") - .insert({ - freelancer_id: ids[0], - client_id: clientId - }) + await tx("freelancer_clients").insert({ + freelancer_id: ids[0], + client_id: clientId, + }); } - }) + }); } if (service_ids) { service_ids.forEach(async (serviceId) => { if (serviceId) { - await tx("freelancer_services") - .insert({ - freelancer_id: ids[0], - service_id: serviceId - }) + await tx("freelancer_services").insert({ + freelancer_id: ids[0], + service_id: serviceId, + }); } - }) + }); } - return ids[0] - }) - - -export const updateFreelancerDetails = (userId: number, f: Freelancer) => - async (tx: Knex.Transaction) => ( - await tx("freelancers").update({ - freelanced_before: f.freelanced_before, - freelancing_goal: f.freelancing_goal, - work_type: f.work_type, - education: f.education, - experience: f.experience, - title: f.title, - bio: f.bio, - facebook_link: f.facebook_link, - twitter_link: f.twitter_link, - telegram_link: f.telegram_link, - discord_link: f.discord_link, - user_id: f.user_id - }) - .where({"user_id": userId}).returning("id") -) - + return ids[0]; + }); +export const updateFreelancerDetails = + (userId: number, f: Freelancer) => async (tx: Knex.Transaction) => + await tx("freelancers") + .update({ + freelanced_before: f.freelanced_before, + freelancing_goal: f.freelancing_goal, + work_type: f.work_type, + education: f.education, + experience: f.experience, + title: f.title, + bio: f.bio, + facebook_link: f.facebook_link, + twitter_link: f.twitter_link, + telegram_link: f.telegram_link, + discord_link: f.discord_link, + user_id: f.user_id, + }) + .where({ user_id: userId }) + .returning("id"); // The search briefs and all these lovely parameters. // Since we are using checkboxes only i unfortunatly ended up using all these parameters. // Because we could have multiple ranges of values and open ended ors. -export const searchBriefs = - async (tx: Knex.Transaction, filter: BriefSqlFilter) => - // select everything that is associated with brief. - fetchAllBriefs()(tx).where(function () { +export const searchBriefs = async ( + tx: Knex.Transaction, + filter: BriefSqlFilter +) => + // select everything that is associated with brief. + fetchAllBriefs()(tx) + .where(function () { if (filter.submitted_range.length > 0) { - this.whereBetween("users.briefs_submitted", [filter.submitted_range[0].toString(), Math.max(...filter.submitted_range).toString()]); + this.whereBetween("users.briefs_submitted", [ + filter.submitted_range[0].toString(), + Math.max(...filter.submitted_range).toString(), + ]); } if (filter.submitted_is_max) { - this.orWhere('users.briefs_submitted', '>=', Math.max(...filter.submitted_range)) + this.orWhere( + "users.briefs_submitted", + ">=", + Math.max(...filter.submitted_range) + ); } }) - .where(function () { - if (filter.experience_range.length > 0) { - this.whereIn("experience_id", filter.experience_range) - } - }) - .where(function () { - if (filter.length_range.length > 0) { - this.whereIn("duration_id", filter.length_range) - } - if (filter.length_is_max) { - this.orWhere('duration_id', '>=', Math.max(...filter.length_range)) - } - }) - .where("headline", "ilike", "%" + filter.search_input + "%") - - - -export const searchFreelancers = - async (tx: Knex.Transaction, filter: FreelancerSqlFilter) => - fetchAllFreelancers()(tx) - .where(function () { - if (filter.skills_range.length > 0) { - this.whereIn('freelancer_skills.skill_id', filter.skills_range) - } - }) - .where(function () { - if (filter.services_range.length > 0) { - this.whereIn('freelancer_services.service_id', filter.services_range) - } - }) - .where(function () { - if (filter.languages_range.length > 0) { - this.whereIn('freelancer_languages.language_id', filter.languages_range) - } - }) - .where("username", "ilike", "%" + filter.search_input + "%") - .where("title", "ilike", "%" + filter.search_input + "%") - .where("bio", "ilike", "%" + filter.search_input + "%") - .debug(true) + .where(function () { + if (filter.experience_range.length > 0) { + this.whereIn("experience_id", filter.experience_range); + } + }) + .where(function () { + if (filter.length_range.length > 0) { + this.whereIn("duration_id", filter.length_range); + } + if (filter.length_is_max) { + this.orWhere( + "duration_id", + ">=", + Math.max(...filter.length_range) + ); + } + }) + .where("headline", "ilike", "%" + filter.search_input + "%"); + +export const searchFreelancers = async ( + tx: Knex.Transaction, + filter: FreelancerSqlFilter +) => + fetchAllFreelancers()(tx) + .where(function () { + if (filter.skills_range.length > 0) { + this.whereIn("freelancer_skills.skill_id", filter.skills_range); + } + }) + .where(function () { + if (filter.services_range.length > 0) { + this.whereIn( + "freelancer_services.service_id", + filter.services_range + ); + } + }) + .where(function () { + if (filter.languages_range.length > 0) { + this.whereIn( + "freelancer_languages.language_id", + filter.languages_range + ); + } + }) + .where("username", "ilike", "%" + filter.search_input + "%") + .where("title", "ilike", "%" + filter.search_input + "%") + .where("bio", "ilike", "%" + filter.search_input + "%") + .debug(true); From c97d67468ad5c03e1f4f9f1f26d9604f4b905485 Mon Sep 17 00:00:00 2001 From: cuteolaf Date: Thu, 16 Mar 2023 19:17:17 -0700 Subject: [PATCH 2/6] fix style: brief details page --- api/public/brief-details.css | 13 +++- api/src/frontend/pages/briefs/details.tsx | 83 +++++++++++++++-------- 2 files changed, 63 insertions(+), 33 deletions(-) diff --git a/api/public/brief-details.css b/api/public/brief-details.css index ec401018..fa8d0b43 100644 --- a/api/public/brief-details.css +++ b/api/public/brief-details.css @@ -220,6 +220,11 @@ p { height: 18%; } +.brief-insights .insight-item { + margin-left: 20px; + margin-bottom: 25px; +} + .brief-insights-stat { display: flex; } @@ -236,6 +241,7 @@ p { .subsection { margin-left: 20px; margin-bottom: 25px; + margin-right: 20px; } .subsection > ul > li { list-style: disc; @@ -309,10 +315,11 @@ p { font-size: 14px; } -.h3, h3 { +.h3, +h3 { font-size: 1.17em !important; } .meet-hiring-team > h3 { - margin-top: 10px -} \ No newline at end of file + margin-top: 10px; +} diff --git a/api/src/frontend/pages/briefs/details.tsx b/api/src/frontend/pages/briefs/details.tsx index 6bbc3d50..617a6dd4 100644 --- a/api/src/frontend/pages/briefs/details.tsx +++ b/api/src/frontend/pages/briefs/details.tsx @@ -4,27 +4,32 @@ import { Brief, User } from "../../models"; import "../../../../public/brief-details.css"; import { getBrief } from "../../services/briefsService"; // import "../../../../public/freelancer-profile.css"; -import TimeAgo from 'javascript-time-ago'; -import en from 'javascript-time-ago/locale/en'; +import TimeAgo from "javascript-time-ago"; +import en from "javascript-time-ago/locale/en"; import { IoMdWallet } from "react-icons/io"; import { FaHandshake } from "react-icons/fa"; import { HiUserGroup } from "react-icons/hi"; -import { fetchUser, fetchUserOrEmail, getCurrentUser, redirect } from "../../utils"; +import { + fetchUser, + fetchUserOrEmail, + getCurrentUser, + redirect, +} from "../../utils"; import { ChatBox } from "../../components"; -import Modal from 'react-bootstrap/Modal'; -import 'bootstrap/dist/css/bootstrap.min.css'; +import Modal from "react-bootstrap/Modal"; +import "bootstrap/dist/css/bootstrap.min.css"; export type BriefProps = { brief: Brief; }; TimeAgo.addDefaultLocale(en); -export const BriefDetails = ({ brief: brief }: BriefProps): JSX.Element => { - const [browsingUser, setBrowsingUser] = useState(); - const [targetUser, setTargetUser] = useState(null); +export const BriefDetails = ({ brief: brief }: BriefProps): JSX.Element => { + const [browsingUser, setBrowsingUser] = useState(); + const [targetUser, setTargetUser] = useState(null); const [showMessageBox, setShowMessageBox] = useState(false); - const isOwnerOfBrief = (browsingUser && browsingUser.id == brief.user_id); + const isOwnerOfBrief = browsingUser && browsingUser.id == brief.user_id; useEffect(() => { async function setup() { @@ -32,30 +37,42 @@ export const BriefDetails = ({ brief: brief }: BriefProps): JSX.Element => { setTargetUser(await fetchUser(brief.user_id)); } setup(); - }, []) + }, []); const timeAgo = new TimeAgo("en-US"); const timePosted = timeAgo.format(new Date(brief.created)); const redirectToApply = () => { redirect(`briefs/${brief.id}/apply`); - } + }; const handleMessageBoxClick = async () => { if (browsingUser) { setShowMessageBox(true); } else { - redirect("login", `/dapp/briefs/${brief.id}/`) + redirect("login", `/dapp/briefs/${brief.id}/`); } - } + }; const renderChat = ( setShowMessageBox(false)}> - {(browsingUser && targetUser) ? :

REACT_APP_GETSTREAM_API_KEY not found

} + {browsingUser && targetUser ? ( + + ) : ( +

REACT_APP_GETSTREAM_API_KEY not found

+ )}
- +
); @@ -74,7 +91,10 @@ export const BriefDetails = ({ brief: brief }: BriefProps): JSX.Element => { {/* TODO: Do we use same styles for both buttons? */}
- {/* TODO: Implement */} @@ -163,13 +183,13 @@ export const BriefDetails = ({ brief: brief }: BriefProps): JSX.Element => { const BioInsights = (
-
+

Brief Insights

-
+

@@ -180,7 +200,7 @@ export const BriefDetails = ({ brief: brief }: BriefProps): JSX.Element => {

-
+

@@ -189,7 +209,7 @@ export const BriefDetails = ({ brief: brief }: BriefProps): JSX.Element => {

-
+

@@ -200,19 +220,22 @@ export const BriefDetails = ({ brief: brief }: BriefProps): JSX.Element => {
- { !isOwnerOfBrief && + {!isOwnerOfBrief && ( <> -
-
-

- Meet the hiring team: -

- +
+
+

Meet the hiring team:

+ +
-
- { browsingUser && showMessageBox && renderChat} + {browsingUser && showMessageBox && renderChat} - } + )}
); From 35322ecee86af3a488d08b8ba698860bd9951f82 Mon Sep 17 00:00:00 2001 From: cuteolaf Date: Thu, 16 Mar 2023 20:34:20 -0700 Subject: [PATCH 3/6] feat: brief dashboard --- api/public/brief-dashboard.css | 224 +++++++++++++++++ api/src/backend/routes/web/index.ts | 26 +- api/src/backend/views/brief-dashboard.pug | 4 + api/src/frontend/components/briefInsights.tsx | 2 +- .../frontend/pages/briefs/brief-dashboard.tsx | 232 ++++++++++++++++++ .../frontend/pages/briefs/hirer-dashboard.tsx | 79 ++++-- api/src/frontend/webpack.config.js | 1 + 7 files changed, 540 insertions(+), 28 deletions(-) create mode 100644 api/public/brief-dashboard.css create mode 100644 api/src/backend/views/brief-dashboard.pug create mode 100644 api/src/frontend/pages/briefs/brief-dashboard.tsx diff --git a/api/public/brief-dashboard.css b/api/public/brief-dashboard.css new file mode 100644 index 00000000..f542222f --- /dev/null +++ b/api/public/brief-dashboard.css @@ -0,0 +1,224 @@ +.text-primary { + color: var(--theme-primary); +} + +.text-secondary { + color: var(--theme-secondary); +} + +.brief-info-container { + display: flex; + flex-direction: column; + gap: 20px; + background: var(--theme-grey-dark); + border: 1px solid var(--theme-white); + border-radius: 20px; + padding: 50px; +} + +.brief-info { + display: flex; + flex-direction: row; +} + +.brief-info .description { + display: flex; + flex-direction: column; + gap: 20px; + flex: 1 0 75%; + margin-right: 5%; +} + +.brief-title { + display: flex; + flex-direction: row; + gap: 36px; +} + +.clickable-text { + color: var(--theme-primary); + cursor: pointer; +} + +.text-inactive { + color: rgba(255, 255, 255, 0.5); +} + +.brief-info .insights { + display: flex; + flex-direction: column; + gap: 20px; + flex: 1 0 20%; +} + +.insight-item { + display: flex; + flex-direction: row; + gap: 28px; + align-items: center; +} + +.insight-value { + display: flex; + flex-direction: column; + gap: 8px; +} + +.insight-value h3 { + margin: 0; + line-height: 1; +} + +.insight-value.milestone { + flex-direction: row; + gap: 5px; +} + +.milestones-complete { + color: var(--theme-primary); + font-weight: 700; +} + +.milestone-progress-indicator { + position: relative; + width: 100%; + height: 4px; + background: #1c2608; + border-radius: 8px; +} + +.milestone-complete { + position: absolute; + top: 0; + left: 0; + bottom: 0; + background: var(--theme-primary); + border-radius: 8px; +} + +.milestone-complete-percentage { + position: absolute; + left: 0; + top: -12px; + font-size: 10px; + line-height: 1; + color: var(--theme-white); + text-align: center; +} + +.milestone-circle { + width: 16px; + height: 16px; + background: var(--theme-primary); + border-radius: 16px; + position: absolute; + top: -6px; + bottom: -6px; +} + +.freelancer-info { + display: flex; + flex-direction: column; + gap: 20px; +} + +.freelancer-profile { + display: flex; + flex-direction: row; + align-items: center; + gap: 32px; +} + +.freelancer-profile-pic { + width: 45px; + height: 45px; +} + +.primary-btn { + margin: 0; +} + +.milestones-container { + display: flex; + flex-direction: column; + gap: 36px; + /* background: var(--theme-white); */ + margin-top: 50px; +} + +.milestone-item { + display: flex; + flex-direction: column; + padding: 20px 50px; + background: #d9d9d9; +} + +.milestone-header { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; + color: var(--theme-black-text); +} + +.milestone-no { + font-size: 39px; + line-height: 36px; + margin-right: 10px; +} + +.milestone-name { + font-size: 24px; + line-height: 1.2; + font-weight: 700; + flex-grow: 1; +} + +.milestone-date { + font-size: 14px; + line-height: 16px; +} + +.milestone-status { + font-size: 20px; + line-height: 23px; + font-weight: 700; +} + +.milestone-details { + display: flex; + flex-direction: column; + margin-top: 10px; +} + +.milestone-release-info { + display: flex; + flex-direction: row; + gap: 10px; + font-size: 14px; + line-height: 16px; + font-weight: 700; + color: var(--theme-black-text); + margin-top: 10px; +} + +.milestone-description { + font-size: 14px; + line-height: 16px; + color: var(--theme-black-text); + margin-top: 16px; +} + +.toggle { + width: 32px; + height: 32px; + padding: 0; +} +.primary-btn.in-dark.toggle { + text-align: center; +} + +.vote { + margin-top: 20px; + align-self: flex-start; +} diff --git a/api/src/backend/routes/web/index.ts b/api/src/backend/routes/web/index.ts index 055d1b3c..158c6fc4 100644 --- a/api/src/backend/routes/web/index.ts +++ b/api/src/backend/routes/web/index.ts @@ -56,19 +56,27 @@ router.get("/briefs/:id", (req, res) => { res.render("brief-details"); }); -router.get("/briefs/:id/apply", function (req, res, next) { - passport.authenticate("jwt", { - session: false, - failureRedirect: `/dapp/login?redirect=/dapp/briefs/${req.params.id}/apply`, - })(req, res, next) -}, (req, res) => { - res.render("submit-proposal"); -}); +router.get( + "/briefs/:id/apply", + function (req, res, next) { + passport.authenticate("jwt", { + session: false, + failureRedirect: `/dapp/login?redirect=/dapp/briefs/${req.params.id}/apply`, + })(req, res, next); + }, + (req, res) => { + res.render("submit-proposal"); + } +); router.get("/briefs/:id/applications", (req, res) => { res.render("brief-applications"); }); +router.get("/briefs/:id/dashboard", (req, res) => { + res.render("brief-dashboard"); +}); + router.get("/briefs/:id/applications/:application_id", (req, res) => { res.render("application-preview"); }); @@ -112,4 +120,4 @@ router.use((_req, res, next) => { res.render("legacy"); }); -export default router; \ No newline at end of file +export default router; diff --git a/api/src/backend/views/brief-dashboard.pug b/api/src/backend/views/brief-dashboard.pug new file mode 100644 index 00000000..c4c6224e --- /dev/null +++ b/api/src/backend/views/brief-dashboard.pug @@ -0,0 +1,4 @@ +extends layout.pug +block content + div(id="brief-dashboard" name="brief-dashboard") + script(src="/public/lib/brief-dashboard.js" type="text/javascript") \ No newline at end of file diff --git a/api/src/frontend/components/briefInsights.tsx b/api/src/frontend/components/briefInsights.tsx index bd20708f..312f5a1d 100644 --- a/api/src/frontend/components/briefInsights.tsx +++ b/api/src/frontend/components/briefInsights.tsx @@ -24,7 +24,7 @@ export const BriefInsights = ({ brief }: BriefInsightsProps) => {
-

{brief.headline }

+

{brief.headline}

View full brief

diff --git a/api/src/frontend/pages/briefs/brief-dashboard.tsx b/api/src/frontend/pages/briefs/brief-dashboard.tsx new file mode 100644 index 00000000..5308435f --- /dev/null +++ b/api/src/frontend/pages/briefs/brief-dashboard.tsx @@ -0,0 +1,232 @@ +import TimeAgo from "javascript-time-ago"; +import en from "javascript-time-ago/locale/en"; +import React, { useState } from "react"; +import ReactDOMClient from "react-dom/client"; +import { Brief } from "../../models"; +import { getBrief } from "../../services/briefsService"; +import { getCurrentUser, redirect } from "../../utils"; +import "../../../../public/brief-dashboard.css"; +import { RiShieldUserLine } from "react-icons/ri"; +import { FaDollarSign, FaRegCalendarAlt } from "react-icons/fa"; +import { ProjectState } from "../../models"; + +interface BriefDashboardProps { + brief: Brief; +} + +TimeAgo.addDefaultLocale(en); + +export const BriefDashboard = ({ brief }: BriefDashboardProps) => { + const timeAgo = new TimeAgo("en-US"); + const timePosted = timeAgo.format(new Date(brief.created)); + + const viewFullBrief = () => { + redirect(`briefs/${brief.id}/`); + }; + + const milestones = [ + { + name: "C++ Network Experts for banking app", + date: "25 February 2023", + percentToRelease: "45%", + fundToRelease: "$45,000", + description: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit ut aliquam, purus sit amet luctus venenatis, lectus magna fringilla urna, porttitor rhoncus dolor purus non enim praesent elementum facilisis leo, vel fringilla est ullamcorper eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in ornare quam viverra orci sagittis eu volutpat odio facilisis mauris sit amet massa vitae tortor condimentum lacinia quis vel eros donec ac odio tempor orci dapibus ultrices in iaculis nunc sed augue lacus, viverra vitae congue eu, consequat ac felis donec et odio pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus faucibus", + status: "Completed", + }, + { + name: "C++ Network Experts for banking app", + date: "25 February 2023", + percentToRelease: "45%", + fundToRelease: "$45,000", + description: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit ut aliquam, purus sit amet luctus venenatis, lectus magna fringilla urna, porttitor rhoncus dolor purus non enim praesent elementum facilisis leo, vel fringilla est ullamcorper eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in ornare quam viverra orci sagittis eu volutpat odio facilisis mauris sit amet massa vitae tortor condimentum lacinia quis vel eros donec ac odio tempor orci dapibus ultrices in iaculis nunc sed augue lacus, viverra vitae congue eu, consequat ac felis donec et odio pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus faucibus", + status: "Open for Voting", + }, + { + name: "C++ Network Experts for banking app", + date: "25 February 2023", + percentToRelease: "45%", + fundToRelease: "$45,000", + description: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit ut aliquam, purus sit amet luctus venenatis, lectus magna fringilla urna, porttitor rhoncus dolor purus non enim praesent elementum facilisis leo, vel fringilla est ullamcorper eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in ornare quam viverra orci sagittis eu volutpat odio facilisis mauris sit amet massa vitae tortor condimentum lacinia quis vel eros donec ac odio tempor orci dapibus ultrices in iaculis nunc sed augue lacus, viverra vitae congue eu, consequat ac felis donec et odio pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus faucibus", + status: "In Progress", + }, + { + name: "C++ Network Experts for banking app", + date: "25 February 2023", + percentToRelease: "45%", + fundToRelease: "$45,000", + description: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit ut aliquam, purus sit amet luctus venenatis, lectus magna fringilla urna, porttitor rhoncus dolor purus non enim praesent elementum facilisis leo, vel fringilla est ullamcorper eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in ornare quam viverra orci sagittis eu volutpat odio facilisis mauris sit amet massa vitae tortor condimentum lacinia quis vel eros donec ac odio tempor orci dapibus ultrices in iaculis nunc sed augue lacus, viverra vitae congue eu, consequat ac felis donec et odio pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus faucibus", + status: "Not Started", + }, + ]; + + const [milestoneVisible, showMilestone] = useState({}); + + const toggleMilestone = (index: number) => { + const current = milestoneVisible[index] || false; + showMilestone({ ...milestoneVisible, [index]: !current }); + }; + + return ( + <> +
+
+
+
+

{brief.headline}

+

+ View full brief +

+
+
+

{brief.description}

+
+
+ Posted {timePosted}{" "} +
+
+
+
+ +
+

Milestone

+
2/4
+
+
+
+
+ {/* + * TODO: Show circles to indicate milestones + */} +
+
+ 50% +
+
+
+ +
+

+ ${Number(brief.budget).toLocaleString()} +

+
+ Budget - fixed +
+
+
+
+ +
+

{brief.duration}

+
Timeline
+
+
+
+
+
+

Freelancer Hired

+
+ +
Idris Muhammad
+ +
+
+
+
+ {milestones.map((m, index) => ( +
+
+
+ Milestone {index + 1} +
+
{m.name}
+
{m.date}
+
+ {m.status} +
+ +
+ {milestoneVisible[index] && ( +
+
+ + Percentage of funds to be released + {" "} + + {m.percentToRelease} + +
+
+ Funding to be released{" "} + + {m.fundToRelease} + +
+
+ {m.description} +
+
+ +
+
+ )} +
+ ))} +
+ + ); +}; + +document.addEventListener("DOMContentLoaded", async (event) => { + let paths = window.location.pathname.split("/"); + let briefId = paths.length >= 2 && parseInt(paths[paths.length - 2]); + const user = await getCurrentUser(); + + if (briefId) { + const brief: Brief = await getBrief(briefId); + if (brief) { + ReactDOMClient.createRoot( + document.getElementById("brief-dashboard")! + ).render(); + } + } +}); diff --git a/api/src/frontend/pages/briefs/hirer-dashboard.tsx b/api/src/frontend/pages/briefs/hirer-dashboard.tsx index c2ae0a39..9cf46159 100644 --- a/api/src/frontend/pages/briefs/hirer-dashboard.tsx +++ b/api/src/frontend/pages/briefs/hirer-dashboard.tsx @@ -31,13 +31,12 @@ export const HirerDashboard = ({ user }: HirerDashboardProps): JSX.Element => { const userBriefs = await getUserBriefs(user.id); setBriefsUnderReview(userBriefs.briefsUnderReview); setAcceptedBriefs(userBriefs.acceptedBriefs); - } + }; setup(); }, []); return (
-
@@ -45,33 +44,48 @@ export const HirerDashboard = ({ user }: HirerDashboardProps): JSX.Element => {

Open Briefs

-

Proposals

+

Proposals

-

{briefsUnderReview.map((brief) => { - const timePosted = timeAgo.format(new Date(brief.created)); + const timePosted = timeAgo.format( + new Date(brief.created) + ); return ( <>

{brief.headline}

- ${Number(brief.budget).toLocaleString()} + + $ + {Number( + brief.budget + ).toLocaleString()} + Created {timePosted}
-

{brief.number_of_applications}

+

+ {brief.number_of_applications} +

- +
- ) + ); })}
@@ -85,22 +99,42 @@ export const HirerDashboard = ({ user }: HirerDashboardProps): JSX.Element => {

{acceptedBriefs.map((brief) => { - const numberOfApprovedMilestones = brief.milestones.filter(milestone => milestone.is_approved).length; + const numberOfApprovedMilestones = + brief.milestones.filter( + (milestone) => milestone.is_approved + ).length; const totalMilestoneCount = brief.milestones.length; - const percent = ((numberOfApprovedMilestones / totalMilestoneCount) ?? 0) * 100; - const timePosted = timeAgo.format(new Date(brief.created)); + const percent = + (numberOfApprovedMilestones / totalMilestoneCount ?? + 0) * 100; + const timePosted = timeAgo.format( + new Date(brief.created) + ); return ( <>

{brief.headline}

- {Number(brief.project.required_funds).toLocaleString()} ${Currency[brief.project.currency_id]} + + {Number( + brief.project.required_funds + ).toLocaleString()}{" "} + $ + { + Currency[ + brief.project.currency_id + ] + } + Created {timePosted}

Milestones

-

{numberOfApprovedMilestones}/{totalMilestoneCount}

+

+ {numberOfApprovedMilestones}/ + {totalMilestoneCount} +

@@ -113,15 +147,24 @@ export const HirerDashboard = ({ user }: HirerDashboardProps): JSX.Element => {
- +
- ) + ); })}
-

+
); }; @@ -132,4 +175,4 @@ document.addEventListener("DOMContentLoaded", async (event) => { document.getElementById("hirer-dashboard")! ).render(); } -}); \ No newline at end of file +}); diff --git a/api/src/frontend/webpack.config.js b/api/src/frontend/webpack.config.js index 1b1cda47..b3b939bd 100644 --- a/api/src/frontend/webpack.config.js +++ b/api/src/frontend/webpack.config.js @@ -20,6 +20,7 @@ module.exports = { "submit-proposal": path.resolve(__dirname, "pages", "briefs", "submit.tsx"), "application-preview": path.resolve(__dirname, "pages", "briefs", "application-preview.tsx"), "hirer-dashboard": path.resolve(__dirname, "pages", "briefs", "hirer-dashboard.tsx"), + "brief-dashboard": path.resolve(__dirname, "pages", "briefs", "brief-dashboard.tsx") }, devtool: process.env.NODE_ENV === "development" ? "inline-source-map" From ea68a78d57637c8ad0a399d5409e44d323c80f96 Mon Sep 17 00:00:00 2001 From: cuteolaf Date: Sat, 18 Mar 2023 03:07:16 -0700 Subject: [PATCH 4/6] style updates: brief dashboard --- api/public/brief-dashboard.css | 28 +++++++++---------- .../frontend/pages/briefs/brief-dashboard.tsx | 11 +++----- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/api/public/brief-dashboard.css b/api/public/brief-dashboard.css index f542222f..5433817a 100644 --- a/api/public/brief-dashboard.css +++ b/api/public/brief-dashboard.css @@ -6,16 +6,19 @@ color: var(--theme-secondary); } -.brief-info-container { - display: flex; - flex-direction: column; - gap: 20px; +.container { background: var(--theme-grey-dark); border: 1px solid var(--theme-white); border-radius: 20px; padding: 50px; } +.brief-info-wrapper { + display: flex; + flex-direction: column; + gap: 20px; +} + .brief-info { display: flex; flex-direction: row; @@ -99,6 +102,7 @@ .milestone-complete-percentage { position: absolute; left: 0; + right: 0; top: -12px; font-size: 10px; line-height: 1; @@ -149,8 +153,6 @@ .milestone-item { display: flex; flex-direction: column; - padding: 20px 50px; - background: #d9d9d9; } .milestone-header { @@ -158,30 +160,26 @@ flex-direction: row; align-items: center; gap: 16px; - color: var(--theme-black-text); + color: var(--theme-white); + line-height: 1; } .milestone-no { - font-size: 39px; - line-height: 36px; + font-size: 40px; margin-right: 10px; } .milestone-name { font-size: 24px; - line-height: 1.2; - font-weight: 700; flex-grow: 1; } .milestone-date { font-size: 14px; - line-height: 16px; } .milestone-status { font-size: 20px; - line-height: 23px; font-weight: 700; } @@ -198,14 +196,14 @@ font-size: 14px; line-height: 16px; font-weight: 700; - color: var(--theme-black-text); + color: var(--theme-white); margin-top: 10px; } .milestone-description { font-size: 14px; line-height: 16px; - color: var(--theme-black-text); + color: var(--theme-white); margin-top: 16px; } diff --git a/api/src/frontend/pages/briefs/brief-dashboard.tsx b/api/src/frontend/pages/briefs/brief-dashboard.tsx index 5308435f..400b0449 100644 --- a/api/src/frontend/pages/briefs/brief-dashboard.tsx +++ b/api/src/frontend/pages/briefs/brief-dashboard.tsx @@ -72,7 +72,7 @@ export const BriefDashboard = ({ brief }: BriefDashboardProps) => { return ( <> -
+
@@ -111,10 +111,7 @@ export const BriefDashboard = ({ brief }: BriefDashboardProps) => { * TODO: Show circles to indicate milestones */}
-
+
50%
@@ -160,10 +157,10 @@ export const BriefDashboard = ({ brief }: BriefDashboardProps) => {
{milestones.map((m, index) => ( -
+
- Milestone {index + 1} + {`Milestone ${index + 1}`}
{m.name}
{m.date}
From 10ec0d08c126d5bfa0d93769cd422ce1f97fec00 Mon Sep 17 00:00:00 2001 From: samelamin Date: Wed, 22 Mar 2023 20:28:18 +0000 Subject: [PATCH 5/6] fix for test --- api/src/frontend/tests/freelancers.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/src/frontend/tests/freelancers.test.tsx b/api/src/frontend/tests/freelancers.test.tsx index 07ab8625..b6392cc0 100644 --- a/api/src/frontend/tests/freelancers.test.tsx +++ b/api/src/frontend/tests/freelancers.test.tsx @@ -8,6 +8,7 @@ function setUp() { "username": "test", "display_name": "test", "password": "test", + "web3_address": "test", "web3Accounts": [], "getstream_token": "test", }; @@ -20,6 +21,7 @@ test("test Freelancer rendering", () => { "username": "test", "display_name": "test", "password": "test", + "web3_address": "test", "web3Accounts": [], "getstream_token": "test", }} />)).toBeTruthy(); @@ -61,7 +63,7 @@ test("test freelancer capturing the input textbox value", () => { fireEvent.click(screen.getByTestId('freelance-goal-2')); fireEvent.click(screen.getByTestId('next-button')); fireEvent.change(screen.getByTestId('title'), {target: {value: 'imbueLegends'}}) - expect(screen.getByTestId('title').value).toEqual('imbueLegends'); + expect((screen.getByTestId('title') as HTMLInputElement).value).toEqual('imbueLegends'); }); test("test freelancer capturing the multiselect languages", () => { From 9653e035c7cd24980adfdac087446b9a83556646 Mon Sep 17 00:00:00 2001 From: ssani7 <66258879+ssani7@users.noreply.github.com> Date: Thu, 23 Mar 2023 19:23:12 +0600 Subject: [PATCH 6/6] Cuteolaf/brief dashboard (#111) * styled brief dashboard * styled brief dashboard --- api/public/brief-dashboard.css | 25 +++++++++++++--- api/public/common.css | 10 +++++-- .../frontend/pages/briefs/brief-dashboard.tsx | 27 ++++++++++------- api/src/legacy/styles/common.css | 29 ++++++++++++------- 4 files changed, 62 insertions(+), 29 deletions(-) diff --git a/api/public/brief-dashboard.css b/api/public/brief-dashboard.css index 5433817a..d16ffa54 100644 --- a/api/public/brief-dashboard.css +++ b/api/public/brief-dashboard.css @@ -1,3 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + .text-primary { color: var(--theme-primary); } @@ -35,7 +39,19 @@ .brief-title { display: flex; flex-direction: row; - gap: 36px; + gap: 15px; + align-items: center; +} + +h1, +h2, +h3 { + font-family: Aeoniktrial, sans-serif; + font-weight: 700; +} + +h3{ + font-size: 20px; } .clickable-text { @@ -50,7 +66,7 @@ .brief-info .insights { display: flex; flex-direction: column; - gap: 20px; + gap: 28px; flex: 1 0 20%; } @@ -58,7 +74,7 @@ display: flex; flex-direction: row; gap: 28px; - align-items: center; + align-items: start; } .insight-value { @@ -74,7 +90,8 @@ .insight-value.milestone { flex-direction: row; - gap: 5px; + align-items: center; + gap: 10px; } .milestones-complete { diff --git a/api/public/common.css b/api/public/common.css index 6a5d6acf..70c04cd9 100644 --- a/api/public/common.css +++ b/api/public/common.css @@ -206,12 +206,16 @@ body { } .str-chat__message .str-chat__message-bubble, -.str-chat__message-input .str-chat__quoted-message-preview .str-chat__quoted-message-bubble { +.str-chat__message-input + .str-chat__quoted-message-preview + .str-chat__quoted-message-bubble { background: dimgray !important; } .str-chat__message-input, .str-chat__message-attachment-card--title, -.str-chat__message-input .str-chat__message-textarea-container .str-chat__message-textarea { +.str-chat__message-input + .str-chat__message-textarea-container + .str-chat__message-textarea { color: var(--theme-ink-color) !important; -} \ No newline at end of file +} diff --git a/api/src/frontend/pages/briefs/brief-dashboard.tsx b/api/src/frontend/pages/briefs/brief-dashboard.tsx index 400b0449..bdf88d37 100644 --- a/api/src/frontend/pages/briefs/brief-dashboard.tsx +++ b/api/src/frontend/pages/briefs/brief-dashboard.tsx @@ -19,6 +19,7 @@ TimeAgo.addDefaultLocale(en); export const BriefDashboard = ({ brief }: BriefDashboardProps) => { const timeAgo = new TimeAgo("en-US"); const timePosted = timeAgo.format(new Date(brief.created)); + const milestonecompleted = 2; const viewFullBrief = () => { redirect(`briefs/${brief.id}/`); @@ -76,7 +77,7 @@ export const BriefDashboard = ({ brief }: BriefDashboardProps) => {
-

{brief.headline}

+

{brief.headline}

{ />

Milestone

-
2/4
+ + 2/4 +

@@ -107,13 +110,15 @@ export const BriefDashboard = ({ brief }: BriefDashboardProps) => { className="milestone-complete" style={{ width: "50%" }} > - {/* - * TODO: Show circles to indicate milestones - */}
-
+
+ {milestones.map((milestone, index) => ( +
=(index+1)?"bg-[#b2ff0b]" :"bg-[#1c2608]"} `}>
+ ))} +
+ {/*
50% -
+
*/}
{ size={24} />
-

+

${Number(brief.budget).toLocaleString()}

@@ -135,7 +140,7 @@ export const BriefDashboard = ({ brief }: BriefDashboardProps) => { size={24} />
-

{brief.duration}

+

{brief.duration}

Timeline
@@ -148,7 +153,7 @@ export const BriefDashboard = ({ brief }: BriefDashboardProps) => { src="/public/profile-image.png" className="freelancer-profile-pic" /> -
Idris Muhammad
+

Idris Muhammad

@@ -162,7 +167,7 @@ export const BriefDashboard = ({ brief }: BriefDashboardProps) => {
{`Milestone ${index + 1}`}
-
{m.name}
+

{m.name}

{m.date}