Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/server/src/handler/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ pub async fn google_callback(
.http_only(true) // Prevent JavaScript access
.expires(Expiration::DateTime(OffsetDateTime::now_utc() + time::Duration::days(5))) // Set an expiration time of 5 days, TODO: read from env?
.secure(!state.is_dev_env) // Send only over HTTPS, comment out for testing
.domain("devsoc.cn")
// .domain("devsoc.cn")
.path("/"); // Available for all paths

let redirect_root = if state.is_dev_env {
Expand Down
2 changes: 1 addition & 1 deletion backend/server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ async fn main() -> Result<(), ChaosError> {
});


let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
let server_task = axum::serve(listener, app);

let _ = tokio::join!(server_task, email_task);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
import { useQuery } from "@tanstack/react-query";
import { getCampaignBySlugs, getCampaignAttachments, getCampaignRoles } from "@/models/campaign";

import { Calendar, ExternalLink, Mail, FileText, Video, Clock, Users, Briefcase, Info, Phone, SquareUserRound } from "lucide-react";
import { dateToString } from "@/lib/utils";
import { Calendar, ExternalLink, Mail, FileText, Video, Clock, Users, Briefcase, Info, Phone, Files } from "lucide-react";
import { dateToString } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { notFound, useParams } from "next/navigation";
import Link from "next/link";

export default function CampaignInfo({ orgSlug, campaignSlug, dict }: { orgSlug: string, campaignSlug: string, dict: any, lang:string }) {
export default function CampaignInfo({ orgSlug, campaignSlug, dict }: { orgSlug: string, campaignSlug: string, dict: any, lang: string }) {
const params = useParams();
const lang = params.lang
const { data: campaignData } = useQuery({
Expand Down Expand Up @@ -173,20 +173,20 @@ export default function CampaignInfo({ orgSlug, campaignSlug, dict }: { orgSlug:
{/* Attachments */}
<div className="p-6">
<h3 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-1">
<SquareUserRound className="w-5 h-5" />
<Files className="w-5 h-5" />
{dict.common.attachments}
</h3>
<div>
{attachmentsData && attachmentsData.length > 0 && attachmentsData.map(attachment => (
<div>
<a
href={attachment.download_url}
target="_blank"
<a
href={attachment.download_url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 underline flex items-center gap-2"
>
<FileText className="w-4 h-4" />
{attachment.file_name} ({(attachment.file_size / 1024).toFixed(2)} KB)
{attachment.file_name} ({(attachment.file_size / 1024).toFixed(2)} KB)
</a>
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,9 @@ export default function CampaignDetails({ campaignId, orgId, dict }: { campaignI
editingMode ? (
<Button variant="outline" onClick={saveUpdatedCampaignDetails} className="cursor-pointer"><Check className="w-4 h-4" /> {dict.dashboard.actions.save}</Button>
) : (
<Button variant="outline" onClick={toggleEditingMode} className="cursor-pointer"><Pencil className="w-4 h-4" /> {dict.dashboard.actions.edit}</Button>
<Link href={`/dashboard/organisation/${orgId}/campaigns/${campaignId}/edit`}>
<Button variant="outline" className="cursor-pointer"><Pencil className="w-4 h-4" /> {dict.dashboard.actions.edit}</Button>
</Link>
)
}
<Button variant="outline" className="cursor-pointer"><Trash className="w-4 h-4" /> {dict.dashboard.actions.delete}</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
"use client";

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { CampaignUpdate, createCampaignRole, getCampaign, getCampaignRoles, updateCampaign, getCampaignAttachments, uploadAttachments, deleteCampaignAttachment, CampaignRole, RoleDetails } from "@/models/campaign";
import { getRatingCategories, createCategory, updateCategory, deleteCategory, RatingCategory } from "@/models/rating";
import { Button } from "@/components/ui/button";
import { Copy, Pencil, Trash, Share, BookOpenCheck, Check, Plus, FormIcon, CircleCheck, Upload, X, FileText, BarChart, ArrowLeft } from "lucide-react";
import { ButtonGroup } from "@/components/ui/button-group";
import { cn } from "@/lib/utils";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import Link from "next/link";
import { getOrganisationUserRole } from "@/models/organisation";
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import SlugInput from "@/components/slug-input";
import { DatePicker } from "@/components/ui/date-picker";
import { snowflakeGenerator } from "@/lib";
import { deleteRole, RoleUpdate, updateRole } from "@/models/role";

export default function CampaignSettings({ campaignId, orgId, dict }: { campaignId: string, orgId: string, dict: any }) {
const queryClient = useQueryClient();

const { data: campaign } = useQuery({
queryKey: [`${campaignId}-campaign-details`],
queryFn: () => getCampaign(campaignId),
});

const { data: roles } = useQuery({
queryKey: [`${campaignId}-campaign-roles`],
queryFn: () => getCampaignRoles(campaignId),
});

const { data: ratingCategories } = useQuery({
queryKey: [`${campaignId}-rating-categories`],
queryFn: () => getRatingCategories(campaignId),
});

const [campaignName, setCampaignName] = useState(campaign?.name ?? "");
const [campaignSlug, setCampaignSlug] = useState(campaign?.campaign_slug ?? "");
const [campaignDescription, setCampaignDescription] = useState(campaign?.description ?? "");
const [campaignStartsAt, setCampaignStartsAt] = useState(new Date(campaign?.starts_at ?? "").toISOString());
const [campaignEndsAt, setCampaignEndsAt] = useState(new Date(campaign?.ends_at ?? "").toISOString());

const { mutateAsync: mutateUpdateCampaignDetails } = useMutation({
mutationFn: (data: CampaignUpdate) => updateCampaign(campaignId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-campaign-details`] });
},
onError: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-campaign-details`] });
},
})

const { mutateAsync: mutateAddRole } = useMutation({
mutationFn: (data: RoleDetails) => createCampaignRole(campaignId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-campaign-roles`] });
},
onError: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-campaign-roles`] });
},
})

const { mutateAsync: mutateUpdateRole } = useMutation({
mutationFn: ({ roleId, data }: { roleId: string, data: RoleUpdate }) => updateRole(roleId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-campaign-roles`] });
},
onError: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-campaign-roles`] });
},
});

const { mutateAsync: mutateDeleteRole } = useMutation({
mutationFn: (roleId: string) => deleteRole(roleId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-campaign-roles`] });
},
onError: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-campaign-roles`] });
},
});

const { mutateAsync: mutateAddRatingCategory } = useMutation({
mutationFn: (name: string) => createCategory(name, campaignId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-rating-categories`] });
},
onError: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-rating-categories`] });
},
})

const { mutateAsync: mutateUpdateRatingCategory } = useMutation({
mutationFn: ({ name, categoryId }: { name: string, categoryId: string }) => updateCategory(name, campaignId, categoryId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-rating-categories`] });
},
onError: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-rating-categories`] });
},
});

const { mutateAsync: mutateDeleteRatingCategory } = useMutation({
mutationFn: (categoryId: string) => deleteCategory(campaignId, categoryId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-rating-categories`] });
},
onError: () => {
queryClient.invalidateQueries({ queryKey: [`${campaignId}-rating-categories`] });
},
});

const handleCampaignDetailsUpdate = (overrides?: Partial<CampaignUpdate>) => {
mutateUpdateCampaignDetails({
name: campaignName,
slug: campaignSlug,
description: campaignDescription,
starts_at: campaignStartsAt,
ends_at: campaignEndsAt,
...overrides,
});
}


return (
<div className="flex flex-col gap-10 max-w-2xl">
<div>
<Link href={`/dashboard/organisation/${orgId}/campaigns/${campaignId}`}>
<div className="flex items-center gap-1">
<ArrowLeft className="w-4 h-4" />
{dict.common.back}
</div>
</Link>
<h1 className="text-2xl font-bold">{dict.dashboard.campaigns.settings.campaign_settings}</h1>
<p className="text-lg font-medium">{campaign?.name}</p>
</div>

{/* General Settings */}
<div className="flex flex-col gap-5">
<h2 className="text-lg font-semibold">{dict.dashboard.campaigns.settings.general_settings}</h2>
<div className="flex flex-col gap-1">
<Label htmlFor="campaign-name">{dict.common.name}</Label>
<Input value={campaignName} onChange={(e) => setCampaignName(e.target.value)} onBlur={() => handleCampaignDetailsUpdate()} />
</div>

<div className="flex flex-col gap-1">
<Label htmlFor="campaign-slug">{dict.common.slug}</Label>
<SlugInput orgId={orgId} name={campaignName} value={campaignSlug} currentSlug={campaign?.campaign_slug} onChange={(value) => setCampaignSlug(value)} onBlur={handleCampaignDetailsUpdate} dict={dict} />
</div>

<div className="flex flex-col gap-1">
<Label htmlFor="campaign-description">{dict.common.description}</Label>
<Textarea className="min-h-[300px]" value={campaignDescription} onChange={(e) => setCampaignDescription(e.target.value)} onBlur={() => handleCampaignDetailsUpdate()} />
</div>

<div className="flex flex-col gap-1">
<DatePicker label={dict.common.starts_at} value={campaignStartsAt} onChange={(value) => { setCampaignStartsAt(value); handleCampaignDetailsUpdate({ starts_at: value }) }} />
</div>

<div className="flex flex-col gap-1">
<DatePicker label={dict.common.ends_at} value={campaignEndsAt} onChange={(value) => { setCampaignEndsAt(value); handleCampaignDetailsUpdate({ ends_at: value }) }} />
</div>
</div>

{/* Role Settings */}
<div className="flex flex-col gap-5">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">{dict.dashboard.campaigns.settings.role_settings}</h2>
<Button
variant="outline"
size="sm"
onClick={() => {
const newRoleId = snowflakeGenerator.generate().toString();
const newRole: RoleDetails = {
id: newRoleId,
campaign_id: campaignId,
name: `New Role ${newRoleId}`,
description: "",
min_available: 1,
max_available: 2,
finalised: false,
};
mutateAddRole(newRole);
}}
>
<Plus className="w-4 h-4 mr-1" />
{dict.common.add}
</Button>
</div>
{roles?.map((r) =>
<RoleCard key={r.id} role={r} updateRole={mutateUpdateRole} deleteRole={mutateDeleteRole} dict={dict} />
)}
</div>

{/* Rating Settings */}
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">{dict.dashboard.campaigns.settings.rating_category_settings}</h2>
<Button
variant="outline"
size="sm"
onClick={() => {
const newCategoryId = snowflakeGenerator.generate().toString();
mutateAddRatingCategory(`New Category ${newCategoryId}`);
}}
>
<Plus className="w-4 h-4 mr-1" />
{dict.common.add}
</Button>
</div>
{ratingCategories?.map((r) =>
<RatingCategoryCard key={r.id} category={r} updateCategory={mutateUpdateRatingCategory} deleteCategory={mutateDeleteRatingCategory} />
)}
</div>

{/* Attachment Settings */}
<div className="flex flex-col gap-2">
<h2 className="text-lg font-semibold">{dict.common.attachments}</h2>
<p>**In development**</p>
</div>
</div>
);
}

function RoleCard({ role, updateRole, deleteRole, dict }: { role: RoleDetails, updateRole: ({ roleId, data }: { roleId: string, data: RoleUpdate }) => void, deleteRole: (roleId: string) => void, dict: any }) {
const [name, setName] = useState(role.name);
const [minAvailable, setMinAvailable] = useState(role.min_available);
const [maxAvailable, setMaxAvailable] = useState(role.max_available);
const [description, setDescription] = useState(role.description ?? "");

const handleUpdate = () => {
updateRole({
roleId: role.id,
data: {
name: name,
min_available: minAvailable,
max_available: maxAvailable,
description: description,
finalised: true,
},
});
}

return (
<div className="border p-2 rounded-md">
<div className="flex gap-1 items-center">
<div className="flex-1">
<Label className="text-xs text-muted-foreground">{dict.common.name}</Label>
<Input value={name} onChange={(e) => setName(e.target.value)} onBlur={handleUpdate} />
</div>
<div className="w-24">
<Label className="text-xs text-muted-foreground">{dict.common.min}</Label>
<Input type="number" value={minAvailable} onChange={(e) => setMinAvailable(parseInt(e.target.value) || 0)} onBlur={handleUpdate} />
</div>
<div className="w-24">
<Label className="text-xs text-muted-foreground">{dict.common.max}</Label>
<Input type="number" value={maxAvailable} onChange={(e) => setMaxAvailable(parseInt(e.target.value) || 0)} onBlur={handleUpdate} />
</div>
<Button variant="ghost" size="icon" className="mt-4" onClick={() => deleteRole(role.id)}>
<Trash className="w-4 h-4 text-destructive" />
</Button>
</div>
<div className="mt-2">
<Label className="text-xs text-muted-foreground">{dict.common.description}</Label>
<Textarea value={description} onChange={(e) => setDescription(e.target.value)} onBlur={handleUpdate} />
</div>
</div>
)
}

function RatingCategoryCard({ category, updateCategory, deleteCategory }: { category: RatingCategory, updateCategory: ({ name, categoryId }: { name: string, categoryId: string }) => void, deleteCategory: (categoryId: string) => void }) {
const [name, setName] = useState(category.name);

const handleUpdate = () => {
updateCategory({
name: name,
categoryId: category.id,
});
}

return (
<div>
<div className="flex gap-1 items-center">
<div className="flex-1">
<Input value={name} onChange={(e) => setName(e.target.value)} onBlur={handleUpdate} />
</div>
<Button variant="ghost" size="icon" onClick={() => deleteCategory(category.id)}>
<Trash className="w-4 h-4 text-destructive" />
</Button>
</div>
</div>
)
}
Loading
Loading