diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 872ff996d..890e9881d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -80,4 +80,4 @@ jobs: working-directory: backend/database-seeding run: | cargo build - cargo run + cargo run -- --email me@example.com diff --git a/backend/database-seeding/Cargo.lock b/backend/database-seeding/Cargo.lock index 221ed00bb..3440bace5 100644 --- a/backend/database-seeding/Cargo.lock +++ b/backend/database-seeding/Cargo.lock @@ -51,6 +51,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -336,6 +386,52 @@ dependencies = [ "stacker", ] +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "const-oid" version = "0.9.6" @@ -484,6 +580,7 @@ name = "database-seeding" version = "0.1.0" dependencies = [ "chrono", + "clap", "dotenvy", "server", "tokio", @@ -933,6 +1030,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" @@ -1284,6 +1387,12 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.15" @@ -1658,6 +1767,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openssl" version = "0.10.73" @@ -2526,7 +2641,7 @@ checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.4.1", "hex", "once_cell", "proc-macro2", @@ -3129,6 +3244,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.17.0" diff --git a/backend/database-seeding/Cargo.toml b/backend/database-seeding/Cargo.toml index a63d4edbd..bdc293f9f 100644 --- a/backend/database-seeding/Cargo.toml +++ b/backend/database-seeding/Cargo.toml @@ -10,5 +10,5 @@ server = { path = "../server" } tokio = { version = "1.34", features = ["macros"]} chrono = { version = "0.4", features = ["serde"] } - -dotenvy = "0.15" \ No newline at end of file +dotenvy = "0.15" +clap = { version = "4.5", features = ["derive"] } diff --git a/backend/database-seeding/src/main.rs b/backend/database-seeding/src/main.rs index 97280dfbe..c687717d3 100644 --- a/backend/database-seeding/src/main.rs +++ b/backend/database-seeding/src/main.rs @@ -1,11 +1,22 @@ +use clap::Parser; use crate::seeder::*; pub mod seeder; +/// Seed data for development use +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Your Google account email - will be used to provide Organisation Admin & SuperUser privileges + #[arg(short, long)] + pub email: String, +} + #[tokio::main] async fn main() { dotenvy::dotenv().expect("Failed to load .env"); + let args = Args::parse(); let seeder = init().await; - seed_database(seeder).await; + seed_database(args.email.to_lowercase(), seeder).await; } diff --git a/backend/database-seeding/src/seeder.rs b/backend/database-seeding/src/seeder.rs index e59c606af..58810321b 100644 --- a/backend/database-seeding/src/seeder.rs +++ b/backend/database-seeding/src/seeder.rs @@ -24,20 +24,20 @@ pub async fn init() -> Seeder { seeder } -pub async fn seed_database(mut seeder: Seeder) { +pub async fn seed_database(dev_email: String, mut seeder: Seeder) { let mut tx = seeder.app_state.db.begin().await.expect("Error beginning DB transaction"); // Super User let users = vec![ User { id: 1, - email: "example.superuser@chaos.devsoc.app".to_string(), - zid: Some("z5555555".to_string()), - name: "Francis Urquhart".to_string(), - pronouns: Some("Ze/Za".to_string()), - gender: Some("Otter".to_string()), - degree_name: Some("Bachelor of Arts".to_string()), - degree_starting_year: Some(1900), + email: dev_email, + zid: Some("z5555558".to_string()), + name: "Chaos Developer".to_string(), + pronouns: None, + gender: None, + degree_name: Some("Bachelor of Chaos Development (Honours)".to_string()), + degree_starting_year: Some(2024), role: UserRole::SuperUser, }, User { @@ -61,16 +61,30 @@ pub async fn seed_database(mut seeder: Seeder) { degree_name: Some("Bachelor of Social Work (Honours)".to_string()), degree_starting_year: Some(2024), role: UserRole::User, - } + }, + User { + id: 4, + email: "example.superuser@chaos.devsoc.app".to_string(), + zid: Some("z5555555".to_string()), + name: "Francis Urquhart".to_string(), + pronouns: Some("Ze/Za".to_string()), + gender: Some("Otter".to_string()), + degree_name: Some("Bachelor of Arts".to_string()), + degree_starting_year: Some(1900), + role: UserRole::SuperUser, + }, ]; for user in users { User::create_user(user, &mut tx).await.expect("Failed seeding Root User"); } - let org_id = Organisation::create(1, + let org_id = Organisation::create( + 1, // User number 1, i.e. Developer "devsoc".to_string(), "UNSW DevSoc".to_string(), + "contact@devsoc.app".to_string(), + Some("https://devsoc.app".to_string()), &mut seeder.app_state.snowflake_generator, &mut tx).await.expect("Failed seeding Organisation"); @@ -92,6 +106,20 @@ pub async fn seed_database(mut seeder: Seeder) { chrono::NaiveDate::from_ymd_opt(2040, 1, 1).unwrap().and_hms_milli_opt(0, 0, 0, 0).unwrap(), Utc, ), + Some(DateTime::::from_naive_utc_and_offset( + chrono::NaiveDate::from_ymd_opt(2041, 1, 1).unwrap().and_hms_milli_opt(0, 0, 0, 0).unwrap(), + Utc, + )), + Some(DateTime::::from_naive_utc_and_offset( + chrono::NaiveDate::from_ymd_opt(2042, 1, 1).unwrap().and_hms_milli_opt(0, 0, 0, 0).unwrap(), + Utc, + )), + Some("in-person".to_string()), + Some(DateTime::::from_naive_utc_and_offset( + chrono::NaiveDate::from_ymd_opt(2043, 1, 1).unwrap().and_hms_milli_opt(0, 0, 0, 0).unwrap(), + Utc, + )), + Some("Resume required".to_string()), &mut tx, &mut seeder.app_state.snowflake_generator, ) diff --git a/backend/migrations/20251206074151_new_campaign_fields.sql b/backend/migrations/20251206074151_new_campaign_fields.sql new file mode 100644 index 000000000..4f8d6ddef --- /dev/null +++ b/backend/migrations/20251206074151_new_campaign_fields.sql @@ -0,0 +1,7 @@ +-- Add migration script here +ALTER TABLE campaigns +ADD COLUMN interview_period_starts_at TIMESTAMPTZ, +ADD COLUMN interview_period_ends_at TIMESTAMPTZ, +ADD COLUMN interview_format TEXT, +ADD COLUMN outcomes_released_at TIMESTAMPTZ, +ADD COLUMN application_requirements TEXT; diff --git a/backend/migrations/20251206074927_organisation_email_website.sql b/backend/migrations/20251206074927_organisation_email_website.sql new file mode 100644 index 000000000..ecdad202e --- /dev/null +++ b/backend/migrations/20251206074927_organisation_email_website.sql @@ -0,0 +1,4 @@ +-- Add migration script here +ALTER TABLE organisations +ADD COLUMN contact_email TEXT NOT NULL, +ADD COLUMN website_url TEXT; \ No newline at end of file diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 98e59cc71..57e396a79 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -49,6 +49,8 @@ impl OrganisationHandler { data.admin, data.slug, data.name, + data.contact_email, + data.website_url, &mut state.snowflake_generator, &mut transaction.tx, ) @@ -444,6 +446,11 @@ impl OrganisationHandler { request_body.description, request_body.starts_at, request_body.ends_at, + request_body.interview_period_starts_at, + request_body.interview_period_ends_at, + request_body.interview_format, + request_body.outcomes_released_at, + request_body.application_requirements, &mut transaction.tx, &mut state.snowflake_generator, ) diff --git a/backend/server/src/models/campaign.rs b/backend/server/src/models/campaign.rs index cac19f7d8..918e31357 100644 --- a/backend/server/src/models/campaign.rs +++ b/backend/server/src/models/campaign.rs @@ -51,6 +51,16 @@ pub struct Campaign { pub created_at: DateTime, /// When the campaign was last updated pub updated_at: DateTime, + /// When interview period begins + pub interview_period_starts_at: Option>, + /// When interview period ends + pub interview_period_ends_at: Option>, + /// Interview format (e.g., "in-person", "online", "hybrid") + pub interview_format: Option, + /// When applicants will be notified of outcomes + pub outcomes_released_at: Option>, + /// Additional application requirements (e.g., "Resume required", "No economics background needed") + pub application_requirements: Option, /// Whether the campaign is published pub published: bool, } @@ -74,6 +84,10 @@ pub struct CampaignDetails { pub organisation_slug: String, /// Name of the organization running the campaign pub organisation_name: String, + /// The organisation's contact email (e.g. contact@devsoc.app) + pub contact_email: String, + /// The organisations website link (e.g. https://devsoc.app) + pub website_url: Option, /// Optional UUID of the campaign's cover image pub cover_image: Option, /// Optional description of the campaign @@ -82,6 +96,16 @@ pub struct CampaignDetails { pub starts_at: DateTime, /// When the campaign stops accepting applications pub ends_at: DateTime, + /// When interview period begins + pub interview_period_starts_at: Option>, + /// When interview period ends + pub interview_period_ends_at: Option>, + /// Interview format (e.g., "in-person", "online", "hybrid") + pub interview_format: Option, + /// When applicants will be notified of outcomes + pub outcomes_released_at: Option>, + /// Additional application requirements (e.g., "Resume required", "No economics background needed") + pub application_requirements: Option, /// Whether the campaign is published pub published: bool, } @@ -114,6 +138,16 @@ pub struct OrganisationCampaign { pub ends_at: DateTime, /// Whether the campaign is published pub published: bool, + /// When interview period begins + pub interview_period_starts_at: Option>, + /// When interview period ends + pub interview_period_ends_at: Option>, + /// Interview format (e.g., "in-person", "online", "hybrid") + pub interview_format: Option, + /// When applicants will be notified of outcomes + pub outcomes_released_at: Option>, + /// Additional application requirements (e.g., "Resume required", "No economics background needed") + pub application_requirements: Option, } /// Data structure for creating a new campaign. @@ -130,7 +164,17 @@ pub struct NewCampaign { /// When the campaign starts accepting applications pub starts_at: DateTime, /// When the campaign stops accepting applications - pub ends_at: DateTime + pub ends_at: DateTime, + /// When interview period begins + pub interview_period_starts_at: Option>, + /// When interview period ends + pub interview_period_ends_at: Option>, + /// Interview format (e.g., "in-person", "online", "hybrid") + pub interview_format: Option, + /// When applicants will be notified of outcomes + pub outcomes_released_at: Option>, + /// Additional application requirements (e.g., "Resume required", "No economics background needed") + pub application_requirements: Option, } /// Data structure for updating an existing campaign. @@ -148,6 +192,16 @@ pub struct CampaignUpdate { pub starts_at: DateTime, /// When the campaign stops accepting applications pub ends_at: DateTime, + /// When interview period begins + pub interview_period_starts_at: Option>, + /// When interview period ends + pub interview_period_ends_at: Option>, + /// Interview format (e.g., "in-person", "online", "hybrid") + pub interview_format: Option, + /// When applicants will be notified of outcomes + pub outcomes_released_at: Option>, + /// Additional application requirements (e.g., "Resume required", "No economics background needed") + pub application_requirements: Option, } /// Response structure for campaign banner updates. @@ -204,8 +258,11 @@ impl Campaign { CampaignDetails, " SELECT c.id, c.slug AS campaign_slug, c.name, c.organisation_id, - o.slug AS organisation_slug, o.name as organisation_name, c.cover_image, - c.description, c.starts_at, c.ends_at, c.published + o.slug AS organisation_slug, o.name as organisation_name, + o.contact_email, o.website_url, c.cover_image, + c.description, c.starts_at, c.ends_at, c.published, c.interview_period_starts_at, + c.interview_period_ends_at, c.interview_format, c.outcomes_released_at, + c.application_requirements FROM campaigns c JOIN organisations o on c.organisation_id = o.id WHERE c.id = $1 @@ -279,8 +336,11 @@ impl Campaign { CampaignDetails, " SELECT c.id, c.slug AS campaign_slug, c.name, c.organisation_id, - o.slug AS organisation_slug, o.name as organisation_name, c.cover_image, - c.description, c.starts_at, c.ends_at, c.published + o.slug AS organisation_slug, o.name as organisation_name, + o.contact_email, o.website_url, c.cover_image, + c.description, c.starts_at, c.ends_at, c.published, + c.interview_period_starts_at, c.interview_period_ends_at, c.interview_format, + c.outcomes_released_at, c.application_requirements FROM campaigns c JOIN organisations o on c.organisation_id = o.id WHERE c.slug = $1 AND o.slug = $2 @@ -315,14 +375,21 @@ impl Campaign { _ = sqlx::query!( " UPDATE campaigns - SET slug = $1, name = $2, description = $3, starts_at = $4, ends_at = $5 - WHERE id = $6 RETURNING id + SET slug = $1, name = $2, description = $3, starts_at = $4, ends_at = $5, + interview_period_starts_at = $6, interview_period_ends_at = $7, + interview_format = $8, outcomes_released_at = $9, application_requirements = $10 + WHERE id = $11 RETURNING id ", update.slug, update.name, update.description, update.starts_at, update.ends_at, + update.interview_period_starts_at, + update.interview_period_ends_at, + update.interview_format, + update.outcomes_released_at, + update.application_requirements, id ) .fetch_one(transaction.deref_mut()) diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 4e3ca1988..f0f58abbb 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -36,6 +36,10 @@ pub struct Organisation { pub created_at: DateTime, /// When the organisation was last updated pub updated_at: DateTime, + /// The organisation's contact email (e.g. contact@devsoc.app) + pub contact_email: String, + /// The organisations website link (e.g. https://devsoc.app) + pub website_url: Option, /// List of campaigns run by this organisation pub campaigns: Vec, // Awaiting Campaign to be complete - remove comment once done /// List of user IDs who are administrators of this organisation @@ -55,6 +59,10 @@ pub struct NewOrganisation { pub name: String, /// ID of the user who will be the initial administrator pub admin: i64, + /// The organisation's contact email (e.g. contact@devsoc.app) + pub contact_email: String, + /// The organisations website link (e.g. https://devsoc.app) + pub website_url: Option, } /// Detailed view of an organisation's information. @@ -74,6 +82,10 @@ pub struct OrganisationDetails { pub logo: Option, /// When the organisation was created pub created_at: DateTime, + /// The organisation's contact email (e.g. contact@devsoc.app) + pub contact_email: String, + /// The organisations website link (e.g. https://devsoc.app) + pub website_url: Option, } /// Possible roles for organisation members. @@ -163,6 +175,8 @@ impl Organisation { admin_id: i64, mut slug: String, name: String, + contact_email: String, + website_url: Option, snowflake_generator: &mut SnowflakeIdGenerator, transaction: &mut Transaction<'_, Postgres>, ) -> Result { @@ -176,12 +190,14 @@ impl Organisation { sqlx::query!( " - INSERT INTO organisations (id, slug, name) - VALUES ($1, $2, $3) + INSERT INTO organisations (id, slug, name, contact_email, website_url) + VALUES ($1, $2, $3, $4, $5) ", id, slug.to_lowercase(), - name + name, + contact_email, + website_url ) .execute(transaction.deref_mut()) .await?; @@ -256,7 +272,7 @@ impl Organisation { let organisation = sqlx::query_as!( OrganisationDetails, " - SELECT id, slug, name, logo, created_at + SELECT id, slug, name, logo, created_at, website_url, contact_email FROM organisations WHERE id = $1 ", @@ -272,7 +288,7 @@ impl Organisation { let organisations = sqlx::query_as!( OrganisationDetails, " - SELECT id, slug, name, logo, created_at + SELECT id, slug, name, logo, created_at, website_url, contact_email FROM organisations ", ) @@ -299,7 +315,7 @@ impl Organisation { let organisation = sqlx::query_as!( OrganisationDetails, " - SELECT id, slug, name, logo, created_at + SELECT id, slug, name, logo, created_at, website_url, contact_email FROM organisations WHERE slug = $1 ", @@ -328,7 +344,7 @@ impl Organisation { let orgs = sqlx::query_as!( OrganisationDetails, " - SELECT o.id, o.slug, o.name, o.logo, o.created_at + SELECT o.id, o.slug, o.name, o.logo, o.created_at, o.website_url, o.contact_email FROM organisations o JOIN organisation_members om ON o.id = om.organisation_id @@ -726,7 +742,9 @@ impl Organisation { " SELECT c.id, c.organisation_id, c.slug as campaign_slug, c.name, c.cover_image, - c.description, c.starts_at, c.ends_at, c.published, o.slug as organisation_slug + c.description, c.starts_at, c.ends_at, c.published, o.slug as organisation_slug, + c.interview_period_starts_at, c.interview_period_ends_at, c.interview_format, + c.outcomes_released_at, c.application_requirements FROM campaigns c LEFT JOIN organisations o on c.organisation_id = o.id WHERE organisation_id = $1 @@ -747,6 +765,11 @@ impl Organisation { description: Option, starts_at: DateTime, ends_at: DateTime, + interview_period_starts_at: Option>, + interview_period_ends_at: Option>, + interview_format: Option, + outcomes_released_at: Option>, + application_requirements: Option, transaction: &mut Transaction<'_, Postgres>, snowflake_id_generator: &mut SnowflakeIdGenerator, ) -> Result { @@ -760,8 +783,8 @@ impl Organisation { sqlx::query!( " - INSERT INTO campaigns (id, organisation_id, slug, name, description, starts_at, ends_at, published) - VALUES ($1, $2, $3, $4, $5, $6, $7, false) + INSERT INTO campaigns (id, organisation_id, slug, name, description, starts_at, ends_at, published, interview_period_starts_at, interview_period_ends_at, interview_format, outcomes_released_at, application_requirements) + VALUES ($1, $2, $3, $4, $5, $6, $7, false, $8, $9, $10, $11, $12) ", new_campaign_id, organisation_id, @@ -769,7 +792,12 @@ impl Organisation { name, description, starts_at, - ends_at + ends_at, + interview_period_starts_at, + interview_period_ends_at, + interview_format, + outcomes_released_at, + application_requirements, ) .execute(transaction.deref_mut()) .await?; diff --git a/frontend-nextjs/bun.lock b/frontend-nextjs/bun.lock index f3e69e79c..98bb79856 100644 --- a/frontend-nextjs/bun.lock +++ b/frontend-nextjs/bun.lock @@ -45,6 +45,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "babel-plugin-react-compiler": "1.0.0", + "baseline-browser-mapping": "^2.9.5", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", "typescript": "^5", @@ -344,6 +345,8 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.5", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], diff --git a/frontend-nextjs/package-lock.json b/frontend-nextjs/package-lock.json index 7ab24b546..096e00cd9 100644 --- a/frontend-nextjs/package-lock.json +++ b/frontend-nextjs/package-lock.json @@ -8,11 +8,13 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@lexical/react": "^0.38.2", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", @@ -23,15 +25,21 @@ "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lexical": "^0.38.2", "lucide-react": "^0.555.0", "moment": "^2.30.1", "next": "16.0.2", + "next-themes": "^0.4.6", "react": "19.2.0", + "react-day-picker": "^9.11.3", "react-dom": "19.2.0", "react-dropzone": "^14.3.8", + "react-icons": "^5.5.0", "react-resizable-panels": "^3.0.6", "remark": "^15.0.1", "remark-html": "^16.0.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0" }, "devDependencies": { @@ -40,6 +48,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "babel-plugin-react-compiler": "1.0.0", + "baseline-browser-mapping": "^2.9.5", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", "typescript": "^5" @@ -72,6 +81,14 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.28.5", "devOptional": true, @@ -84,6 +101,11 @@ "node": ">=6.9.0" } }, + "node_modules/@date-fns/tz": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", + "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==" + }, "node_modules/@floating-ui/core": { "version": "1.7.3", "license": "MIT", @@ -99,6 +121,20 @@ "@floating-ui/utils": "^0.2.10" } }, + "node_modules/@floating-ui/react": { + "version": "0.27.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", + "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", + "dependencies": { + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, "node_modules/@floating-ui/react-dom": { "version": "2.1.6", "license": "MIT", @@ -230,10 +266,323 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lexical/clipboard": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.38.2.tgz", + "integrity": "sha512-dDShUplCu8/o6BB9ousr3uFZ9bltR+HtleF/Tl8FXFNPpZ4AXhbLKUoJuucRuIr+zqT7RxEv/3M6pk/HEoE6NQ==", + "dependencies": { + "@lexical/html": "0.38.2", + "@lexical/list": "0.38.2", + "@lexical/selection": "0.38.2", + "@lexical/utils": "0.38.2", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/code": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.38.2.tgz", + "integrity": "sha512-wpqgbmPsfi/+8SYP0zI2kml09fGPRhzO5litR9DIbbSGvcbawMbRNcKLO81DaTbsJRnBJiQvbBBBJAwZKRqgBw==", + "dependencies": { + "@lexical/utils": "0.38.2", + "lexical": "0.38.2", + "prismjs": "^1.30.0" + } + }, + "node_modules/@lexical/devtools-core": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.38.2.tgz", + "integrity": "sha512-hlN0q7taHNzG47xKynQLCAFEPOL8l6IP79C2M18/FE1+htqNP35q4rWhYhsptGlKo4me4PtiME7mskvr7T4yqA==", + "dependencies": { + "@lexical/html": "0.38.2", + "@lexical/link": "0.38.2", + "@lexical/mark": "0.38.2", + "@lexical/table": "0.38.2", + "@lexical/utils": "0.38.2", + "lexical": "0.38.2" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/@lexical/dragon": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.38.2.tgz", + "integrity": "sha512-riOhgo+l4oN50RnLGhcqeUokVlMZRc+NDrxRNs2lyKSUdC4vAhAmAVUHDqYPyb4K4ZSw4ebZ3j8hI2zO4O3BbA==", + "dependencies": { + "@lexical/extension": "0.38.2", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/extension": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.38.2.tgz", + "integrity": "sha512-qbUNxEVjAC0kxp7hEMTzktj0/51SyJoIJWK6Gm790b4yNBq82fEPkksfuLkRg9VQUteD0RT1Nkjy8pho8nNamw==", + "dependencies": { + "@lexical/utils": "0.38.2", + "@preact/signals-core": "^1.11.0", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/hashtag": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.38.2.tgz", + "integrity": "sha512-jNI4Pv+plth39bjOeeQegMypkjDmoMWBMZtV0lCynBpkkPFlfMnyL9uzW/IxkZnX8LXWSw5mbWk07nqOUNTCrA==", + "dependencies": { + "@lexical/text": "0.38.2", + "@lexical/utils": "0.38.2", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/history": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.38.2.tgz", + "integrity": "sha512-QWPwoVDMe/oJ0+TFhy78TDi7TWU/8bcDRFUNk1nWgbq7+2m+5MMoj90LmOFwakQHnCVovgba2qj+atZrab1dsQ==", + "dependencies": { + "@lexical/extension": "0.38.2", + "@lexical/utils": "0.38.2", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/html": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.38.2.tgz", + "integrity": "sha512-pC5AV+07bmHistRwgG3NJzBMlIzSdxYO6rJU4eBNzyR4becdiLsI4iuv+aY7PhfSv+SCs7QJ9oc4i5caq48Pkg==", + "dependencies": { + "@lexical/selection": "0.38.2", + "@lexical/utils": "0.38.2", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/link": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.38.2.tgz", + "integrity": "sha512-UOKTyYqrdCR9+7GmH6ZVqJTmqYefKGMUHMGljyGks+OjOGZAQs78S1QgcPEqltDy+SSdPSYK7wAo6gjxZfEq9g==", + "dependencies": { + "@lexical/extension": "0.38.2", + "@lexical/utils": "0.38.2", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/list": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.38.2.tgz", + "integrity": "sha512-OQm9TzatlMrDZGxMxbozZEHzMJhKxAbH1TOnOGyFfzpfjbnFK2y8oLeVsfQZfZRmiqQS4Qc/rpFnRP2Ax5dsbA==", + "dependencies": { + "@lexical/extension": "0.38.2", + "@lexical/selection": "0.38.2", + "@lexical/utils": "0.38.2", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/mark": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.38.2.tgz", + "integrity": "sha512-U+8KGwc3cP5DxSs15HfkP2YZJDs5wMbWQAwpGqep9bKphgxUgjPViKhdi+PxIt2QEzk7WcoZWUsK1d2ty/vSmg==", + "dependencies": { + "@lexical/utils": "0.38.2", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/markdown": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.38.2.tgz", + "integrity": "sha512-ykQJ9KUpCs1+Ak6ZhQMP6Slai4/CxfLEGg/rSHNVGbcd7OaH/ICtZN5jOmIe9ExfXMWy1o8PyMu+oAM3+AWFgA==", + "dependencies": { + "@lexical/code": "0.38.2", + "@lexical/link": "0.38.2", + "@lexical/list": "0.38.2", + "@lexical/rich-text": "0.38.2", + "@lexical/text": "0.38.2", + "@lexical/utils": "0.38.2", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/offset": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.38.2.tgz", + "integrity": "sha512-uDky2palcY+gE6WTv6q2umm2ioTUnVqcaWlEcchP6A310rI08n6rbpmkaLSIh3mT2GJQN2QcN2x0ct5BQmKIpA==", + "dependencies": { + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/overflow": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.38.2.tgz", + "integrity": "sha512-f6vkTf+YZF0EuKvUK3goh4jrnF+Z0koiNMO+7rhSMLooc5IlD/4XXix4ZLiIktUWq4BhO84b82qtrO+6oPUxtw==", + "dependencies": { + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/plain-text": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.38.2.tgz", + "integrity": "sha512-xRYNHJJFCbaQgr0uErW8Im2Phv1nWHIT4VSoAlBYqLuVGZBD4p61dqheBwqXWlGGJFk+MY5C5URLiMicgpol7A==", + "dependencies": { + "@lexical/clipboard": "0.38.2", + "@lexical/dragon": "0.38.2", + "@lexical/selection": "0.38.2", + "@lexical/utils": "0.38.2", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/react": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.38.2.tgz", + "integrity": "sha512-M3z3MkWyw3Msg4Hojr5TnO4TzL71NVPVNGoavESjdgJbTdv1ezcQqjE4feq+qs7H9jytZeuK8wsEOJfSPmNd8w==", + "dependencies": { + "@floating-ui/react": "^0.27.16", + "@lexical/devtools-core": "0.38.2", + "@lexical/dragon": "0.38.2", + "@lexical/extension": "0.38.2", + "@lexical/hashtag": "0.38.2", + "@lexical/history": "0.38.2", + "@lexical/link": "0.38.2", + "@lexical/list": "0.38.2", + "@lexical/mark": "0.38.2", + "@lexical/markdown": "0.38.2", + "@lexical/overflow": "0.38.2", + "@lexical/plain-text": "0.38.2", + "@lexical/rich-text": "0.38.2", + "@lexical/table": "0.38.2", + "@lexical/text": "0.38.2", + "@lexical/utils": "0.38.2", + "@lexical/yjs": "0.38.2", + "lexical": "0.38.2", + "react-error-boundary": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/@lexical/rich-text": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.38.2.tgz", + "integrity": "sha512-eFjeOT7YnDZYpty7Zlwlct0UxUSaYu53uLYG+Prs3NoKzsfEK7e7nYsy/BbQFfk5HoM1pYuYxFR2iIX62+YHGw==", + "dependencies": { + "@lexical/clipboard": "0.38.2", + "@lexical/dragon": "0.38.2", + "@lexical/selection": "0.38.2", + "@lexical/utils": "0.38.2", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/selection": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.38.2.tgz", + "integrity": "sha512-eMFiWlBH6bEX9U9sMJ6PXPxVXTrihQfFeiIlWLuTpEIDF2HRz7Uo1KFRC/yN6q0DQaj7d9NZYA6Mei5DoQuz5w==", + "dependencies": { + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/table": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.38.2.tgz", + "integrity": "sha512-uu0i7yz0nbClmHOO5ZFsinRJE6vQnFz2YPblYHAlNigiBedhqMwSv5bedrzDq8nTTHwych3mC63tcyKIrM+I1g==", + "dependencies": { + "@lexical/clipboard": "0.38.2", + "@lexical/extension": "0.38.2", + "@lexical/utils": "0.38.2", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/text": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.38.2.tgz", + "integrity": "sha512-+juZxUugtC4T37aE3P0l4I9tsWbogDUnTI/mgYk4Ht9g+gLJnhQkzSA8chIyfTxbj5i0A8yWrUUSw+/xA7lKUQ==", + "dependencies": { + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/utils": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.38.2.tgz", + "integrity": "sha512-y+3rw15r4oAWIEXicUdNjfk8018dbKl7dWHqGHVEtqzAYefnEYdfD2FJ5KOTXfeoYfxi8yOW7FvzS4NZDi8Bfw==", + "dependencies": { + "@lexical/list": "0.38.2", + "@lexical/selection": "0.38.2", + "@lexical/table": "0.38.2", + "lexical": "0.38.2" + } + }, + "node_modules/@lexical/yjs": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.38.2.tgz", + "integrity": "sha512-fg6ZHNrVQmy1AAxaTs8HrFbeNTJCaCoEDPi6pqypHQU3QVfqr4nq0L0EcHU/TRlR1CeduEPvZZIjUUxWTZ0u8g==", + "dependencies": { + "@lexical/offset": "0.38.2", + "@lexical/selection": "0.38.2", + "lexical": "0.38.2" + }, + "peerDependencies": { + "yjs": ">=13.5.22" + } + }, "node_modules/@next/env": { "version": "16.0.2", "license": "MIT" }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.2.tgz", + "integrity": "sha512-E6rxUdkZX5sZjLduXphiMuRJAmvsxWi5IivD0kRLLX5cjNLOs2PjlSyda+dtT3iqE6vxaRGV3oQMnQiJU8F+Ig==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.2.tgz", + "integrity": "sha512-QNXdjXVFtb35vImDJtXqYlhq8A2mHLroqD8q4WCwO+IVnVoQshhcEVWJlP9UB/dOC6Wh782BbTHqGzKQwlCSkQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.2.tgz", + "integrity": "sha512-dM9yEB35GZAW3r+w88iGEz7OkJjSYSd4pKyl4KwSXx8cLWMpWaX1WW42dCAKXCWWQhVUXUZAEx38yfpEZ1/IJg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.2.tgz", + "integrity": "sha512-hiNysPK1VeK5MGNmuKLnj3Y4lkaffvAlXin404QpxYkNCBms/Bk0msZHey5lUNq8FV50PY6I9CgY+c/NK+xeLg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-linux-x64-gnu": { "version": "16.0.2", "cpu": [ @@ -262,6 +611,45 @@ "node": ">= 10" } }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.2.tgz", + "integrity": "sha512-TMWE1h44d0WRyq0yQI/0W5A7nZUoiwE2Sdg43wt2Q1IoadU5Ky00G3cJ2mSnbetwL7+eFyM7BQgx+Fonpz6T8w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.2.tgz", + "integrity": "sha512-+8SqzDhau/PNsWdcagnoz6ltOM9IcsqagdTFsEELNOty0+lNh5hwO5oUFForPOywTbM+d3tPLo5m20VdEBDf3Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@preact/signals-core": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.12.1.tgz", + "integrity": "sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "license": "MIT" @@ -690,6 +1078,59 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "license": "MIT", @@ -1423,6 +1864,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.5.tgz", + "integrity": "sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001754", "funding": [ @@ -1507,6 +1957,20 @@ "devOptional": true, "license": "MIT" }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==" + }, "node_modules/debug": { "version": "4.4.3", "license": "MIT", @@ -1666,6 +2130,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "peer": true, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/jiti": { "version": "2.6.1", "dev": true, @@ -1680,6 +2154,32 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/lexical": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.38.2.tgz", + "integrity": "sha512-JJmfsG3c4gwBHzUGffbV7ifMNkKAWMCnYE3xJl87gty7hjyV5f3xq7eqTjP5HFYvO4XpjJvvWO2/djHp5S10tw==" + }, + "node_modules/lib0": { + "version": "0.2.114", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", + "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", + "peer": true, + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lightningcss": { "version": "1.30.2", "dev": true, @@ -2341,6 +2841,15 @@ } } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "funding": [ @@ -2407,6 +2916,14 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -2433,6 +2950,26 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.12.0.tgz", + "integrity": "sha512-t8OvG/Zrciso5CQJu5b1A7yzEmebvST+S3pOVQJWxwjjVngyG/CA2htN/D15dLI4uTEuLLkbZyS4YYt480FAtA==", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "date-fns": "^4.1.0", + "date-fns-jalali": "^4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "19.2.0", "license": "MIT", @@ -2460,6 +2997,25 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-error-boundary": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz", + "integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -2651,6 +3207,15 @@ "@img/sharp-win32-x64": "0.34.5" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "license": "BSD-3-Clause", @@ -2699,6 +3264,11 @@ } } }, + "node_modules/tabbable": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==" + }, "node_modules/tailwind-merge": { "version": "3.4.0", "license": "MIT", @@ -2907,6 +3477,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/yjs": { + "version": "13.6.27", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", + "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", + "peer": true, + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/zwitch": { "version": "2.0.4", "license": "MIT", @@ -2914,96 +3501,6 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.2.tgz", - "integrity": "sha512-E6rxUdkZX5sZjLduXphiMuRJAmvsxWi5IivD0kRLLX5cjNLOs2PjlSyda+dtT3iqE6vxaRGV3oQMnQiJU8F+Ig==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.2.tgz", - "integrity": "sha512-QNXdjXVFtb35vImDJtXqYlhq8A2mHLroqD8q4WCwO+IVnVoQshhcEVWJlP9UB/dOC6Wh782BbTHqGzKQwlCSkQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.2.tgz", - "integrity": "sha512-dM9yEB35GZAW3r+w88iGEz7OkJjSYSd4pKyl4KwSXx8cLWMpWaX1WW42dCAKXCWWQhVUXUZAEx38yfpEZ1/IJg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.2.tgz", - "integrity": "sha512-hiNysPK1VeK5MGNmuKLnj3Y4lkaffvAlXin404QpxYkNCBms/Bk0msZHey5lUNq8FV50PY6I9CgY+c/NK+xeLg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.2.tgz", - "integrity": "sha512-TMWE1h44d0WRyq0yQI/0W5A7nZUoiwE2Sdg43wt2Q1IoadU5Ky00G3cJ2mSnbetwL7+eFyM7BQgx+Fonpz6T8w==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.2.tgz", - "integrity": "sha512-+8SqzDhau/PNsWdcagnoz6ltOM9IcsqagdTFsEELNOty0+lNh5hwO5oUFForPOywTbM+d3tPLo5m20VdEBDf3Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/frontend-nextjs/package.json b/frontend-nextjs/package.json index ca590a0b7..db7236ac0 100644 --- a/frontend-nextjs/package.json +++ b/frontend-nextjs/package.json @@ -48,6 +48,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "babel-plugin-react-compiler": "1.0.0", + "baseline-browser-mapping": "^2.9.5", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", "typescript": "^5" diff --git a/frontend-nextjs/src/app/[lang]/campaign/[orgSlug]/[campaignSlug]/campaign-info.tsx b/frontend-nextjs/src/app/[lang]/campaign/[orgSlug]/[campaignSlug]/campaign-info.tsx new file mode 100644 index 000000000..64f3af70e --- /dev/null +++ b/frontend-nextjs/src/app/[lang]/campaign/[orgSlug]/[campaignSlug]/campaign-info.tsx @@ -0,0 +1,336 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { getCampaignBySlugs, getCampaignRoles } from "@/models/campaign"; + +import { Calendar, ExternalLink, Mail, FileText, Video, Clock, Users, Briefcase, Info, Phone } from "lucide-react"; +import { dateToString } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { notFound } from "next/navigation"; +import Link from "next/link"; + +export default function CampaignInfo({ orgSlug, campaignSlug, dict }: { orgSlug: string, campaignSlug: string, dict: any }) { + const { data: campaignData } = useQuery({ + queryKey: [`${orgSlug}-${campaignSlug}-campaign-details`], + queryFn: () => getCampaignBySlugs(orgSlug, campaignSlug) + }); + + if (!campaignData) return notFound(); + + const { data: roleData } = useQuery({ + queryKey: [`${campaignData?.id}-campaign-roles`], + queryFn: () => getCampaignRoles(campaignData!.id) + }); + + return ( +
+ {/* Hero Section */} +
+ Campaign cover +
+ +
+

+ {campaignData.name} +

+ {campaignData.organisation_name && ( +

+ {campaignData.organisation_name} +

+ )} +
+
+ + {/* Main Content */} +
+ {/* Flexbox container for main layout */} +
+ {/* Left Column */} +
+ {/* Main Content Container with unified card */} +
+ {/* Campaign Description */} + {campaignData.description && ( +
+

+ + {dict.common.about} +

+

+ {campaignData.description} +

+
+ )} + + {/* Divider */} + {campaignData.description && campaignData.application_requirements && ( +
+ )} + + {/* Application Requirements */} + {campaignData.application_requirements && ( +
+

+ + {dict.common.application_requirements} +

+

+ {campaignData.application_requirements} +

+
+ )} + + {/* Divider */} + {(campaignData.application_requirements || campaignData.description) && campaignData.interview_format && ( +
+ )} + + {/* Interview Information */} + {campaignData.interview_format && ( +
+

+

+
+
+ + {dict.common.interview_format} + + {campaignData.interview_period_starts_at && campaignData.interview_period_ends_at && ( + + {dict.common.interview_period} + + )} + +
+
+ + {campaignData.interview_format} + + {campaignData.interview_period_starts_at && campaignData.interview_period_ends_at && ( + + {dateToString(campaignData.interview_period_starts_at.toString())} - {dateToString(campaignData.interview_period_ends_at.toString())} + + )} +
+
+
+ )} + + {/* Divider */} +
+ + {/* Available Roles */} +
+

+ + {dict.common.available_roles} +

+
+ {roleData?.map(role => ( +
+
+

+ {role.name} +

+ + {role.max_available} + +
+ {role.description && ( +

+ {role.description} +

+ )} +
+ ))} + {roleData?.length === 0 && ( +

+ No roles available for this campaign. +

+ )} +
+
+ + {/* Divider */} +
+ + {/* Timeline Section */} +
+

+ + {dict.common.recruitment_timeline} +

+ +
+ {/* Applications Open */} +
+
+
+
+
+
+

+ {dict.common.applications_open} +

+

+ {dateToString(campaignData.starts_at)} +

+
+
+ + {/* Applications Close */} +
+
+
+
+
+
+

+ {dict.common.applications_close} +

+

+ {dateToString(campaignData.ends_at)} +

+
+
+ + {/* Interviews */} + {campaignData.interview_period_starts_at && campaignData.interview_period_ends_at && ( +
+
+
+
+
+
+

+ {dict.common.interviews} +

+

+ {dateToString(campaignData.interview_period_starts_at.toString())} - {dateToString(campaignData.interview_period_ends_at.toString())} +

+
+
+ )} + + {/* Results Announced */} + {campaignData.outcomes_released_at && ( +
+
+
+
+
+

+ {dict.common.results_announced} +

+

+ {dateToString(campaignData.outcomes_released_at.toString())} +

+
+
+ )} +
+
+ + {/* Divider */} +
+ + {/* Contact Information Section */} +
+

+ + {dict.common.contact_information} +

+ +
+ {campaignData.website_url && ( + + )} + + {campaignData.contact_email && ( + + )} +
+
+
+
+ + {/* Right Column - Sidebar */} +
+
+ {/* Apply Card */} +
+

+ {dict.common.apply_now} +

+ +
+
+ +
+

+ {dict.common.application_deadline} +

+

+ {dateToString(campaignData.ends_at)} +

+
+
+ + {campaignData.outcomes_released_at && ( +
+ +
+

+ {dict.common.results_announced} +

+

+ {dateToString(campaignData.outcomes_released_at.toString())} +

+
+
+ )} +
+ + {/* TODO: If user is logged in, check for existing application and change text to "Continue Application" */} + + + +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend-nextjs/src/app/[lang]/campaign/[orgSlug]/[campaignSlug]/page.tsx b/frontend-nextjs/src/app/[lang]/campaign/[orgSlug]/[campaignSlug]/page.tsx index 233149630..e07298e7d 100644 --- a/frontend-nextjs/src/app/[lang]/campaign/[orgSlug]/[campaignSlug]/page.tsx +++ b/frontend-nextjs/src/app/[lang]/campaign/[orgSlug]/[campaignSlug]/page.tsx @@ -1,209 +1,23 @@ -"use client"; +import { getDictionary } from "@/app/[lang]/dictionaries"; +import { getCampaignBySlugs } from "@/models/campaign"; +import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query"; +import CampaignInfo from "./campaign-info"; -import { useQuery } from "@tanstack/react-query"; -import { getCampaignDetails } from "@/models/campaign"; -import { useParams } from "next/navigation"; -import { CalendarIcon, UsersIcon } from "lucide-react"; -import { dateToString } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { MapPin, ExternalLink, Mail } from "lucide-react"; -export default function CampaignPage() { - const { orgSlug, campaignSlug } = useParams<{ - lang: string; - orgSlug: string; - campaignSlug: string; - }>(); +export default async function CampaignPage({ params }: { params: Promise<{ orgSlug: string, campaignSlug: string, lang: string }> }) { + const { orgSlug, campaignSlug, lang } = await params; + const dict = await getDictionary(lang); + const queryClient = new QueryClient(); - const { data, error, isLoading } = useQuery({ - queryKey: [`${orgSlug}-${campaignSlug}-recruitment-page`], - queryFn: () => getCampaignDetails(orgSlug, campaignSlug), + await queryClient.prefetchQuery({ + queryKey: [`${orgSlug}-${campaignSlug}-campaign-details`], + queryFn: () => getCampaignBySlugs(orgSlug, campaignSlug), }); - if (!orgSlug || !campaignSlug) return
Waiting for params…
; - if (isLoading) return
Loading...
- if (error) return
{String(error)}
- if (!data) { - return
Where's the data!
- } - return ( -
- {/* Hero Section with Cover Image */} -
- {data.cover_image && ( - Campaign cover - )} -
- -
-
- - OPEN - -
-

- {data.name || "Campaign Recruitment"} -

-
-
- - {/* Main Content */} -
-
- {/* Left Column - Main Content */} -
- {/* Organization Info */} -
-

- {data.organisation_name || "Development Society"} -

-

- {data.description || "Join the leading tech society at UNSW! We're looking for passionate individuals to help run events, workshops, and hackathons."} -

-
- - {/* Description */} -
-

- About This Campaign -

-
-

- {data.description} -

-
-
- - {/* Available Roles - Placeholder */} -
-

- Available Roles -

-

- Role details will be displayed here -

-
-
- - {/* Right Column - Sidebar */} -
- {/* Apply Card */} -
-

- Apply Now -

- -
-
- -
-

- Application Deadline -

-

- {dateToString(data.ends_at)} -

-
-
-
- - -
- - {/* Timeline Card */} -
-

- Recruitment Timeline -

- -
-
-
-
-
-
-
-

- Applications Open -

-

- {dateToString(data.starts_at)} -

-
-
- -
-
-
-
-
-
-

- Applications Close -

-

- {dateToString(data.ends_at)} -

-
-
- -
-
-
-
-
-

- Interviews -

-

- TBA -

-
-
-
-
+ + + + ) - {/* Contact Card */} -
-

- Contact Information -

- -
-
- - - UNSW Sydney, Kensington Campus - -
- - - - devsoc.com - - - - - recruitment@devsoc.com - -
-
-
-
-
-
- ); } \ No newline at end of file diff --git a/frontend-nextjs/src/app/[lang]/dashboard/organisation/[orgId]/campaigns/[campaignId]/campaign-details.tsx b/frontend-nextjs/src/app/[lang]/dashboard/organisation/[orgId]/campaigns/[campaignId]/campaign-details.tsx index bda4e1f20..f79f9fa8d 100644 --- a/frontend-nextjs/src/app/[lang]/dashboard/organisation/[orgId]/campaigns/[campaignId]/campaign-details.tsx +++ b/frontend-nextjs/src/app/[lang]/dashboard/organisation/[orgId]/campaigns/[campaignId]/campaign-details.tsx @@ -18,7 +18,7 @@ import Link from "next/link"; import { getOrganisationUserRole } from "@/models/organisation"; import EditDetail from "./edit-detail"; import { useEffect, useState } from "react"; -import { CampaignRole } from "@/models/campaign"; +import { RoleDetails } from "@/models/campaign"; import CampaignDates from "./campaign-dates"; import { RoleUpdate, deleteRole, updateRole } from "@/models/role"; import { toast } from "sonner"; @@ -26,7 +26,7 @@ import { remark } from "remark"; import html from "remark-html"; import CopyButton from "@/components/copy-button"; -interface ClientRole extends CampaignRole { +interface ClientRole extends RoleDetails { deleting: boolean; new: boolean; } @@ -41,7 +41,7 @@ interface CampaignDetailsData { export type CampaignUpdateKeys = keyof CampaignDetailsData | "roleName" | "roleMinAvailable" | "roleMaxAvailable"; -function compareCampaignRoles(roles: CampaignRole[], clientRoles: ClientRole[]): boolean { +function compareCampaignRoles(roles: RoleDetails[], clientRoles: ClientRole[]): boolean { if (roles.length !== clientRoles.length) return false; return roles.every( (role, index) => role.id === clientRoles[index].id && @@ -78,7 +78,7 @@ export default function CampaignDetails({ campaignId, orgId, dict }: { campaignI }); const { mutateAsync: mutateCreateCampaignRole } = useMutation({ - mutationFn: (data: CampaignRole) => createCampaignRole(campaignId, data), + mutationFn: (data: RoleDetails) => createCampaignRole(campaignId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [`${campaignId}-campaign-roles`] }); }, @@ -113,14 +113,14 @@ export default function CampaignDetails({ campaignId, orgId, dict }: { campaignI }); const [clientRoles, setClientRoles] = useState([]); - const [newRoleId, setNewRoleId] = useState(0); + const [newRoleId, setNewRoleId] = useState("0"); const [hoveredDeleteIndex, setHoveredDeleteIndex] = useState(null); const [descriptionHtmlState, setDescriptionHtmlState] = useState(""); const [dateError, setDateError] = useState(false); - const [roleNameError, setRoleNameError] = useState<{ [key: number]: boolean }>( + const [roleNameError, setRoleNameError] = useState<{ [key: string]: boolean }>( clientRoles.reduce((acc, role) => ({ ...acc, [role.id]: false }), {}) ); - const [rolePositionError, setRolePositionError] = useState<{ [key: number]: boolean }>( + const [rolePositionError, setRolePositionError] = useState<{ [key: string]: boolean }>( clientRoles.reduce((acc, role) => ({ ...acc, [role.id]: false }), {}) ); const [updatedCampaignDetails, setUpdatedCampaignDetails] = useState(null); @@ -142,6 +142,7 @@ export default function CampaignDetails({ campaignId, orgId, dict }: { campaignI setClientRoles([...clientRoles, { // temp id for table row key id: newRoleId, + campaign_id: campaignId, name: "New Role", description: "", min_available: 1, @@ -153,7 +154,7 @@ export default function CampaignDetails({ campaignId, orgId, dict }: { campaignI setNewRoleId(newRoleId + 1); }; - const handleDeleteRole = (roleId: number) => { + const handleDeleteRole = (roleId: string) => { if (!editingMode) return; const deletingRole = clientRoles.find((r) => r.id === roleId); @@ -306,6 +307,7 @@ export default function CampaignDetails({ campaignId, orgId, dict }: { campaignI } else if (role.new === true) { return mutateCreateCampaignRole({ id: role.id, + campaign_id: campaignId, name: role.name, description: role.description ?? undefined, min_available: role.min_available, @@ -339,7 +341,7 @@ export default function CampaignDetails({ campaignId, orgId, dict }: { campaignI await mutationResult; setUpdatedCampaignDetails(null); setEditingMode(false); - setNewRoleId(0); + setNewRoleId("0"); } catch (err) { setClientRoles(roles?.map((role) => ({ ...role, deleting: false, new: false })) ?? []); } diff --git a/frontend-nextjs/src/dictionaries/en.json b/frontend-nextjs/src/dictionaries/en.json index 84d28a862..e0a814dd0 100644 --- a/frontend-nextjs/src/dictionaries/en.json +++ b/frontend-nextjs/src/dictionaries/en.json @@ -1,85 +1,105 @@ { - "common": { - "organisation": "Organisation", - "name": "Name", - "slug": "Slug", - "starts_at": "Starts At", - "ends_at": "Ends At", - "published": "Published", - "dashboard": "Dashboard", - "profile": "Profile", - "campaigns": "Campaigns", - "settings": "Settings", - "roles": "Roles", - "description": "Description", - "questions": "Questions", - "common_questions": "Common Questions", - "back": "Back", - "application": "Application", - "status": "Status", - "email": "Email" + "common": { + "organisation": "Organisation", + "name": "Name", + "slug": "Slug", + "starts_at": "Starts At", + "ends_at": "Ends At", + "published": "Published", + "dashboard": "Dashboard", + "profile": "Profile", + "campaigns": "Campaigns", + "settings": "Settings", + "roles": "Roles", + "description": "Description", + "about": "About", + "questions": "Questions", + "common_questions": "Common Questions", + "back": "Back", + "application": "Application", + "application_requirements": "Application Requirements", + "interview_details": "Interview Details", + "interview_format": "Interview Format", + "interview_period": "Interview Period", + "available_roles": "Available Roles", + "recruitment_timeline": "Recruitment Timeline", + + "applications_open": "Applications Open", + "applications_close": "Applications Close", + "interviews": "Interviews", + "results_announced": "Results Announced", + + "contact_information": "Contact Information", + "visit_website": "Visit Website", + + "apply_now": "Apply Now", + "application_deadline": "Application Deadline", + "apply": "Apply", + + "status": "Status", + "email": "Email" + }, + "dashboard": { + "email_templates": "Email Templates", + "members": { + "role": "Role", + "members": "Members", + "admins": "Admins", + "users": "Users", + "remove_member": "Remove Member", + "add_member": "Add Member", + "invite_member": "Invite Member", + "admin_edit_block": "Cannot change admin members", + "admin_edit_block_description": "Admins cannot be changed in the dashboard. Please email admin@chaos.devsoc.app to update your admins list." }, - "dashboard": { - "email_templates": "Email Templates", - "members": { - "role": "Role", - "members": "Members", - "admins": "Admins", - "users": "Users", - "remove_member": "Remove Member", - "add_member": "Add Member", - "invite_member": "Invite Member", - "admin_edit_block": "Cannot change admin members", - "admin_edit_block_description": "Admins cannot be changed in the dashboard. Please email admin@chaos.devsoc.app to update your admins list." - }, - "slug_tooltip": "User-friendly URL alias for the campaign", - "suggested_slug": "Suggested slug", - "slug_not_available": "Slug not available", - "sidebar": { - "create_organisation": "Create Organisation" - }, - "campaigns": { - "copy_campaign_id": "Copy ID", - "copy_campaign_link": "Copy Campaign Link", - "edit_campaign": "Edit Campaign", - "share_link": "Share Link", - "review_applications": "Review Applications", - "manage_questions": "Manage Questions", - "publish": "Publish", - "published": "Published", - "draft": "Draft", - "roles": { - "number_of_positions": "Number of Positions", - "add_role": "Add Role" - }, - "application_review_page": { - "no_application_selected": "No application selected", - "click_on_application_to_view": "Click on an application on the left to view it", - "application_rating": "Application rating", - "review_score": "Score", - "review_comment": "Comments (optional)", - "write_your_review_here": "Write your review here...", - "application_not_rated": "Application not rated" - } - }, - "email": { - "name": "Name", - "subject": "Subject", - "body": "Body", - "delete_confirmation": "This will permanently delete the email template and cannot be undone.", - "edit_template": "Edit Template" - }, - "actions": { - "new": "New", - "edit": "Edit", - "delete": "Delete", - "preview": "Preview", - "duplicate": "Duplicate", - "cancel": "Cancel", - "save": "Save", - "destructive_confirm": "Are you absolutely sure?", - "confirm_understand": "Ok", - "invite": "Invite" - } + "slug_tooltip": "User-friendly URL alias for the campaign", + "suggested_slug": "Suggested slug", + "slug_not_available": "Slug not available", + "sidebar": { + "create_organisation": "Create Organisation" + }, + "campaigns": { + "copy_campaign_id": "Copy ID", + "copy_campaign_link": "Copy Campaign Link", + "edit_campaign": "Edit Campaign", + "share_link": "Share Link", + "review_applications": "Review Applications", + "manage_questions": "Manage Questions", + "publish": "Publish", + "published": "Published", + "draft": "Draft", + "roles": { + "number_of_positions": "Number of Positions", + "add_role": "Add Role" + }, + "application_review_page": { + "no_application_selected": "No application selected", + "click_on_application_to_view": "Click on an application on the left to view it", + "application_rating": "Application rating", + "review_score": "Score", + "review_comment": "Comments (optional)", + "write_your_review_here": "Write your review here...", + "application_not_rated": "Application not rated" + } + }, + "email": { + "name": "Name", + "subject": "Subject", + "body": "Body", + "delete_confirmation": "This will permanently delete the email template and cannot be undone.", + "edit_template": "Edit Template" + }, + "actions": { + "new": "New", + "edit": "Edit", + "delete": "Delete", + "preview": "Preview", + "duplicate": "Duplicate", + "cancel": "Cancel", + "save": "Save", + "destructive_confirm": "Are you absolutely sure?", + "confirm_understand": "Ok", + "invite": "Invite" } -} \ No newline at end of file + } +} diff --git a/frontend-nextjs/src/dictionaries/zh.json b/frontend-nextjs/src/dictionaries/zh.json index 5059e7c5a..f36a34ebd 100644 --- a/frontend-nextjs/src/dictionaries/zh.json +++ b/frontend-nextjs/src/dictionaries/zh.json @@ -1,87 +1,106 @@ { - "common": { - "organisation": "组织", - "name": "名称", - "slug": "别名", - "starts_at": "开始时间", - "ends_at": "结束时间", - "published": "已发布", - "dashboard": "仪表盘", - "profile": "个人资料", - "campaigns": "活动", - "settings": "设置", - "roles": "角色", - "description": "详细描述", - "questions": "问题", - "common_questions": "通用问题", - "back": "返回", - "application": "申请", - "status": "状态", - "email": "邮箱" + "common": { + "organisation": "组织", + "name": "名称", + "slug": "别名", + "starts_at": "开始时间", + "ends_at": "结束时间", + "published": "已发布", + "dashboard": "仪表盘", + "profile": "个人资料", + "campaigns": "活动", + "settings": "设置", + "roles": "角色", + "description": "详细描述", + "about": "关于", + "questions": "问题", + "common_questions": "通用问题", + "back": "返回", + "application": "申请", + "application_requirements": "申请要求", + "interview_details": "面试详情", + "interview_format": "面试格式", + "interview_period": "面试周期", + "available_roles": "可用的角色", + "recruitment_timeline": "招聘时间线", + + "applications_open": "申请开放", + "applications_close": "申请关闭", + "interviews": "面试", + "results_announced": "结果公布", + + "contact_information": "联系信息", + "visit_website": "访问网站", + + "apply_now": "立即申请", + "application_deadline": "申请截止日期", + "apply": "申请", + + "status": "状态", + "email": "邮箱" + }, + "dashboard": { + "email_templates": "邮件模板", + "members": { + "role": "角色", + "members": "成员", + "admins": "管理员", + "users": "用户", + "remove_member": "移除成员", + "add_member": "添加成员", + "invite_member": "邀请成员", + "admin_edit_block": "无法更改管理员", + "admin_edit_block_description": "管理员无法在仪表盘更改。请发送邮件至 admin@chaos.devsoc.app 更新管理员列表。" }, - "dashboard": { - "email_templates": "邮件模板", - "members": { - "role": "角色", - "members": "成员", - "admins": "管理员", - "users": "用户", - "remove_member": "移除成员", - "add_member": "添加成员", - "invite_member": "邀请成员", - "admin_edit_block": "无法更改管理员", - "admin_edit_block_description": "管理员无法在仪表盘更改。请发送邮件至 admin@chaos.devsoc.app 更新管理员列表。" - }, - "slug_tooltip": "[NOT PROVIDED]", - "suggested_slug": "建议别名", - "slug_not_available": "别名不可用", - "sidebar": { - "create_organisation": "创建组织" - }, - "campaigns": { - "copy_campaign_id": "复制 ID", - "copy_campaign_link": "复制活动链接", - "edit_campaign": "编辑活动", - "share_link": "分享链接", - "review_applications": "查看", - "manage_questions": "管理问题", - "publish": "发布", - "published": "已发布", - "draft": "草稿", - "roles": { - "number_of_positions": "职位数量", - "add_role": "添加角色" - }, - "application_review_page": { - "no_application_selected": "未选择要评估的申请", - "click_on_application_to_view": "点击左侧以查看申请", - "application_rating": "评估申请", - "review_score": "评分", - "review_comment": "评论 (可选)", - "write_your_review_here": "在这里写下你的评论...", - "application_not_rated": "未评估" - } - }, - "email": { - "name": "名称", - "subject": "主题", - "body": "正文", - "delete_confirmation": "这将永久删除邮件模板并无法撤销", - "edit_template": "编辑模板" - }, - "actions": { - "new": "新建", - "edit": "编辑", - "save": "保存", - "delete": "删除", - "preview": "预览", - "duplicate": "复制", - "cancel": "取消", - "save": "保存", - "destructive_confirm": "你确定吗", - "confirm_understand": "确定", - "invite": "邀请" - } + "slug_tooltip": "[NOT PROVIDED]", + "suggested_slug": "建议别名", + "slug_not_available": "别名不可用", + "sidebar": { + "create_organisation": "创建组织" + }, + "campaigns": { + "copy_campaign_id": "复制 ID", + "copy_campaign_link": "复制活动链接", + "edit_campaign": "编辑活动", + "share_link": "分享链接", + "review_applications": "查看", + "manage_questions": "管理问题", + "publish": "发布", + "published": "已发布", + "draft": "草稿", + "roles": { + "number_of_positions": "职位数量", + "add_role": "添加角色" + }, + "application_review_page": { + "no_application_selected": "未选择要评估的申请", + "click_on_application_to_view": "点击左侧以查看申请", + "application_rating": "评估申请", + "review_score": "评分", + "review_comment": "评论 (可选)", + "write_your_review_here": "在这里写下你的评论...", + "application_not_rated": "未评估" + } + }, + "email": { + "name": "名称", + "subject": "主题", + "body": "正文", + "delete_confirmation": "这将永久删除邮件模板并无法撤销", + "edit_template": "编辑模板" + }, + "actions": { + "new": "新建", + "edit": "编辑", + "save": "保存", + "delete": "删除", + "preview": "预览", + "duplicate": "复制", + "cancel": "取消", + "save": "保存", + "destructive_confirm": "你确定吗", + "confirm_understand": "确定", + "invite": "邀请" } + } } - diff --git a/frontend-nextjs/src/models/campaign.ts b/frontend-nextjs/src/models/campaign.ts index 5f8ca6fd8..b7cbb64cd 100644 --- a/frontend-nextjs/src/models/campaign.ts +++ b/frontend-nextjs/src/models/campaign.ts @@ -6,18 +6,25 @@ import { createProperSlug } from "./slug"; export interface CampaignDetails { /// Unique identifier for the campaign - id: number; + id: string; campaign_slug: string; name: string; organisation_id: string; organisation_slug: string; organisation_name: string; + contact_email: string, + website_url: string, cover_image: string | null; /// Optional description of the campaign description: string | null; starts_at: string; ends_at: string; published: boolean; + interview_period_starts_at: Date | null, + interview_period_ends_at: Date | null, + interview_format: string | null, + outcomes_released_at: Date | null, + application_requirements: string | null, } export interface NewCampaign { @@ -38,7 +45,6 @@ export interface CampaignUpdate { ends_at: string; } - export async function getCampaign(campaignId: string): Promise { return await apiRequest(`/api/v1/campaign/${campaignId}`); } @@ -50,10 +56,11 @@ export async function updateCampaign(campaignId: string, campaign: CampaignUpdat }); } -export interface CampaignRole { - id: number; +export interface RoleDetails { + id: string; + campaign_id: string; name: string; - description: string; + description?: string; min_available: number; max_available: number; finalised: boolean; @@ -96,8 +103,8 @@ export async function setCampaignCoverImage(campaignId: string): Promise(`/api/v1/campaign/${campaignId}/banner`, { method: "PATCH" }); } -export async function getCampaignRoles(campaignId: string): Promise { - return await apiRequest(`/api/v1/campaign/${campaignId}/roles`); +export async function getCampaignRoles(campaignId: string): Promise { + return await apiRequest(`/api/v1/campaign/${campaignId}/roles`); } export async function createCampaignRole(campaignId: string, role: RoleUpdate): Promise { @@ -111,7 +118,7 @@ export async function getCampaignApplications(campaignId: string): Promise(`/api/v1/campaign/${campaignId}/applications`); } -export async function getCampaignDetails(orgSlug: string, campaignSlug: string): Promise { +export async function getCampaignBySlugs(orgSlug: string, campaignSlug: string): Promise { // return await apiRequest(`/api/v1/campaign/${orgSlug}/${campaignSlug}`); return await apiRequest(`/api/v1/organisation/slug/${orgSlug}/campaign/slug/${campaignSlug}`); } \ No newline at end of file