Skip to content

Commit ac07210

Browse files
authored
fix: create campaign improvements (#816)
* Revert "Availabilities CRUD (#735)" This reverts commit 01457e4. * fix: date validation and slug update * fix: make banner instruction more descriptive
1 parent ba43cc3 commit ac07210

8 files changed

Lines changed: 102 additions & 197 deletions

File tree

backend/server/src/handler/availabilities.rs

Lines changed: 0 additions & 149 deletions
This file was deleted.

backend/server/src/handler/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
pub mod answer;
2121
pub mod application;
2222
pub mod auth;
23-
pub mod availabilities;
2423
pub mod campaign;
2524
pub mod comment;
2625
pub mod email_template;

backend/server/src/models/app.rs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use crate::handler::answer::AnswerHandler;
22
use crate::handler::application::ApplicationHandler;
33
use crate::handler::auth::{google_auth_init, google_callback, logout, DevLoginHandler};
4-
use crate::handler::availabilities::AvailabilitiesHandler;
54
use crate::handler::campaign::CampaignHandler;
65
use crate::handler::comment::CommentHandler;
76
use crate::handler::email_template::EmailTemplateHandler;
@@ -13,7 +12,6 @@ use crate::handler::rating::RatingHandler;
1312
use crate::handler::role::RoleHandler;
1413
use crate::handler::role_status::RoleStatusHandler;
1514
use crate::handler::user::UserHandler;
16-
use crate::models::availabilities::Availability;
1715
use crate::models::email::{ChaosEmail, EmailCredentials};
1816
use crate::models::error::ChaosError;
1917
use crate::models::storage::Storage;
@@ -180,11 +178,6 @@ pub async fn init_app_state() -> AppState {
180178
}
181179
}
182180

183-
#[derive(Serialize)]
184-
pub struct AvailabilitiesMessage {
185-
pub availabilities: Vec<Availability>,
186-
}
187-
188181
pub async fn app() -> Result<(Router, AppState), ChaosError> {
189182
let state = init_app_state().await;
190183
let state_clone = state.clone();
@@ -567,10 +560,6 @@ pub async fn app() -> Result<(Router, AppState), ChaosError> {
567560
"/api/v1/invite/:code",
568561
get(InviteHandler::get).post(InviteHandler::use_invite),
569562
)
570-
.route(
571-
"/api/v1/availabilities/:user_id/:campaign_id",
572-
get(AvailabilitiesHandler::get).patch(AvailabilitiesHandler::update),
573-
)
574563
.layer(cors)
575564
.with_state(state);
576565

backend/server/src/models/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ pub mod answer;
1111
pub mod app;
1212
pub mod application;
1313
pub mod auth;
14-
pub mod availabilities;
1514
pub mod campaign;
1615
pub mod comment_last_read;
1716
pub mod comment;

frontend-nextjs/src/app/[lang]/dashboard/organisation/[orgId]/campaigns/new/new-campaign.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { getOrganisationById } from "@/models/organisation";
2222
import { uploadFile } from "@/models/file";
2323
import { createCategory } from "@/models/rating";
2424
import SlugInput from "@/components/slug-input";
25+
import { toast } from "sonner";
2526

2627
export default function CampaignNewForm({ orgId, dict }: { orgId: string, dict: any }) {
2728
const queryClient = useQueryClient();
@@ -43,8 +44,16 @@ export default function CampaignNewForm({ orgId, dict }: { orgId: string, dict:
4344
return;
4445
}
4546

47+
if (startDate >= endDate) {
48+
toast.error(dict.dashboard.campaigns.start_date_before_end_date);
49+
return;
50+
}
51+
52+
// Prefer the edited slug; fall back to the autofilled suggestion from the name.
53+
const usedSlug = (slug || suggestedSlug || createProperSlug(name)).trim();
54+
4655
setSaving(true);
47-
const res = await createCampaign(name, description, startDate, endDate, orgId, slug);
56+
const res = await createCampaign(name, description, startDate, endDate, orgId, usedSlug);
4857
const campaignId = res.id;
4958
const bannerUpdate = await setCampaignCoverImage(campaignId);
5059

@@ -56,9 +65,16 @@ export default function CampaignNewForm({ orgId, dict }: { orgId: string, dict:
5665
redirect(`/dashboard/organisation/${orgId}/campaigns/${campaignId}`);
5766
}
5867

59-
const handleNameChange = (name: string) => {
60-
setName(name);
61-
setSuggestedSlug(createProperSlug(name));
68+
const handleNameChange = (nextName: string) => {
69+
setName(nextName);
70+
const nextSlug = createProperSlug(nextName);
71+
setSuggestedSlug((prevSuggested) => {
72+
// Only keep overwriting slug while it still matches the previous autofill.
73+
setSlug((prevSlug) =>
74+
!prevSlug || prevSlug === prevSuggested ? nextSlug : prevSlug
75+
);
76+
return nextSlug;
77+
});
6278
}
6379

6480
return (
@@ -101,7 +117,7 @@ export default function CampaignNewForm({ orgId, dict }: { orgId: string, dict:
101117
<DatePicker label={dict.common.starts_at} value={startDate} onChange={(value) => setStartDate(value)} />
102118
<DatePicker label={dict.common.ends_at} value={endDate} onChange={(value) => setEndDate(value)} />
103119
<div>
104-
<Button disabled={!name || !selectedImage || !startDate || !endDate || saving || !slugAvailable || !slug} onClick={async () => await submitData()}>{dict.dashboard.actions.save}</Button>
120+
<Button disabled={!name || !selectedImage || !startDate || !endDate || saving || !slugAvailable || !(slug || suggestedSlug)} onClick={async () => await submitData()}>{dict.dashboard.actions.save}</Button>
105121
</div>
106122
</div>
107123
</div>
Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,85 @@
11
import { Input } from "@/components/ui/input";
22
import { checkCampaignSlugAvailability } from "@/models/campaign";
33
import { createProperSlug } from "@/models/slug";
4-
import { useState } from "react";
5-
6-
export default function SlugInput({ orgId, name, value, currentSlug, onChange, onBlur, updateSlugAvailable, dict }: { orgId: string, name: string, value: string, currentSlug?: string, onChange: (value: string) => void, onBlur: () => void, updateSlugAvailable?: (available: boolean) => void, dict: any }) {
7-
const [slugAvailable, setSlugAvailable] = useState(true);
8-
9-
const checkSlugAvailability = async () => {
10-
if (value !== currentSlug) {
11-
try {
12-
await checkCampaignSlugAvailability(orgId, value);
13-
setSlugAvailable(true);
14-
updateSlugAvailable?.(true);
15-
} catch (_) {
16-
setSlugAvailable(false);
17-
updateSlugAvailable?.(false);
18-
}
19-
}
20-
}
4+
import { useEffect, useState } from "react";
5+
6+
export default function SlugInput({
7+
orgId,
8+
name,
9+
value,
10+
currentSlug,
11+
onChange,
12+
onBlur,
13+
updateSlugAvailable,
14+
dict,
15+
}: {
16+
orgId: string;
17+
name: string;
18+
value: string;
19+
placeholder?: string;
20+
currentSlug?: string;
21+
onChange: (value: string) => void;
22+
onBlur: () => void;
23+
updateSlugAvailable?: (available: boolean) => void;
24+
dict: any;
25+
}) {
26+
const [slugAvailable, setSlugAvailable] = useState(true);
27+
const [valueOverride, setValueOverride] = useState(false);
28+
29+
const effectiveSlug = valueOverride ? value : (currentSlug ?? value);
2130

22-
const handleBlur = () => {
23-
checkSlugAvailability();
24-
onBlur();
31+
// Keep parent slug state in sync with the autofilled suggestion
32+
// until the user manually edits the field.
33+
useEffect(() => {
34+
if (valueOverride) return;
35+
if (!currentSlug) return;
36+
if (value === currentSlug) return;
37+
onChange(currentSlug);
38+
}, [currentSlug, value, valueOverride, onChange]);
39+
40+
const checkSlugAvailability = async (slugToCheck: string) => {
41+
if (!slugToCheck) return;
42+
43+
try {
44+
await checkCampaignSlugAvailability(orgId, slugToCheck);
45+
setSlugAvailable(true);
46+
updateSlugAvailable?.(true);
47+
} catch (_) {
48+
setSlugAvailable(false);
49+
updateSlugAvailable?.(false);
2550
}
51+
};
52+
53+
const handleBlur = () => {
54+
checkSlugAvailability(effectiveSlug);
55+
onBlur();
56+
};
2657

58+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
59+
onChange(e.target.value);
60+
setValueOverride(true);
61+
};
2762

28-
return (
29-
<>
30-
{name && <p className="text-sm text-gray-500">{dict.dashboard.suggested_slug}: <span className="bg-gray-100 border rounded px-1">{name ? createProperSlug(name) : ""}</span></p>}
31-
<Input className="max-w-[300px]" type="text" value={value} onChange={(e) => onChange(e.target.value)} onBlur={handleBlur} />
32-
<p className="text-red-500 text-xs">{!slugAvailable && dict.dashboard.slug_not_available}</p>
33-
</>
34-
)
35-
}
63+
return (
64+
<>
65+
{name && (
66+
<p className="text-sm text-gray-500">
67+
{dict.dashboard.suggested_slug}:{" "}
68+
<span className="bg-gray-100 border rounded px-1">
69+
{name ? createProperSlug(name) : ""}
70+
</span>
71+
</p>
72+
)}
73+
<Input
74+
className="max-w-[300px]"
75+
type="text"
76+
value={effectiveSlug}
77+
onChange={(e) => handleChange(e)}
78+
onBlur={handleBlur}
79+
/>
80+
<p className="text-red-500 text-xs">
81+
{!slugAvailable && dict.dashboard.slug_not_available}
82+
</p>
83+
</>
84+
);
85+
}

frontend-nextjs/src/components/ui/image-upload.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ import {
8383
</div>
8484

8585
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
86-
<span className="font-semibold">Drag an image</span>
86+
<span className="font-semibold">Upload a banner</span>
8787
</p>
8888
<p className="text-xs text-gray-400 dark:text-gray-400">
89-
Select a image or drag here to upload directly
89+
Select an image or drag here to upload directly
9090
</p>
9191
</div>
9292
)}

frontend-nextjs/src/dictionaries/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
"applicant_pipeline": "Applicant Pipeline",
120120
"no_roles_available": "No roles available",
121121
"no_rating_categories_available": "No rating categories available",
122+
"start_date_before_end_date": "Start date must be before end date",
122123
"settings": {
123124
"campaign_settings": "Campaign Settings",
124125
"general_settings": "General Settings",

0 commit comments

Comments
 (0)