Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
50 changes: 50 additions & 0 deletions app/src/components/contribute/ContributionFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const createVariantContData = (): VariantContribution => ({
const createObjectiveContData = (): ObjectiveContribution => ({
title: "",
summary: "",
subjects: [],
targets: [],
featured: "",
subObjectives: [],
Expand All @@ -73,6 +74,32 @@ export default function ContributionFlow({ type, setType }: PropTypes) {
const [objectiveContData, setObjectiveContData] =
useState<ObjectiveContribution>(createObjectiveContData);

useEffect(() => {
const savedGuide = sessionStorage.getItem("guide_cont_data");
if (savedGuide) {
try {
setGuideContData(JSON.parse(savedGuide));
} catch (e) {}
}
const savedObj = sessionStorage.getItem("objective_cont_data");
if (savedObj) {
try {
setObjectiveContData(JSON.parse(savedObj));
} catch (e) {}
}
}, []);

useEffect(() => {
sessionStorage.setItem("guide_cont_data", JSON.stringify(guideContData));
}, [guideContData]);

useEffect(() => {
sessionStorage.setItem(
"objective_cont_data",
JSON.stringify(objectiveContData)
);
}, [objectiveContData]);

const StepperInstance = useMemo(() => {
if (!type) {
return defineStepper(typeStep);
Expand Down Expand Up @@ -128,6 +155,28 @@ function Inner({
objectiveContData: ObjectiveContribution;
setObjectiveContData: Dispatch<SetStateAction<ObjectiveContribution>>;
}) {
const hasHydratedStep = useRef(false);

useEffect(() => {
if (stepper.current?.id) {
sessionStorage.setItem("cont_step", stepper.current.id);
}
}, [stepper.current?.id]);

useEffect(() => {
if (hasHydratedStep.current) return;
if (type !== null) {
const savedStep = sessionStorage.getItem("cont_step");
if (savedStep) {
requestAnimationFrame(() => {
try {
stepper.goTo(savedStep);
} catch (e) {}
});
}
hasHydratedStep.current = true;
}
}, [type, stepper]);
const pickType = (value: ContributionType) => {
if (type !== value) {
setGuideContData(createGuideContData());
Expand Down Expand Up @@ -388,6 +437,7 @@ function Inner({
/>
<PreviewObjective
Stepper={Stepper}
objectiveContData={objectiveContData}
onPublish={publish}
submitting={submitting}
/>
Expand Down
18 changes: 14 additions & 4 deletions app/src/components/contribute/StepperActionHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@ type PropTypes = {
title: string;
Stepper: any;
nextDisabled?: boolean;
hideNext?: boolean;
onNextClick?: (e: React.MouseEvent) => void;
hideBackBtn?: boolean;
submitting?: boolean;
onSaveDraft?: () => void;
onPublish?: () => void;
publishText?: string;
};

export const StepperActionHeader = ({
title,
Stepper,
nextDisabled,
hideNext,
onNextClick,
submitting,
hideBackBtn,
onSaveDraft,
onPublish,
publishText = "Submit for Review",
}: PropTypes) => {
return (
<>
Expand Down Expand Up @@ -52,13 +58,17 @@ export const StepperActionHeader = ({
disabled={submitting}
onClick={onPublish}
>
Submit for Review
{publishText}
</button>
) : (
<Stepper.Next className="btn-pri" disabled={nextDisabled}>
) : !hideNext ? (
<Stepper.Next
className="btn-pri"
disabled={nextDisabled}
onClick={onNextClick}
>
Next
</Stepper.Next>
)}
) : null}
</div>
</div>

Expand Down
71 changes: 67 additions & 4 deletions app/src/components/contribute/steps/ObjectiveDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import type { Dispatch, SetStateAction } from "react";
import type { ObjectiveContribution } from "@/types/contributions";

Expand All @@ -14,6 +15,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Combobox } from "@/components/ui/combobox";

import guidesData from "@/data/guides.json";
import subjectsData from "@/data/subjects.json";

type PropTypes = {
Stepper: any;
Expand All @@ -26,6 +28,7 @@ export const ObjectiveDetails = ({
objectiveContData,
setObjectiveContData,
}: PropTypes) => {
const [showErrors, setShowErrors] = useState(false);
const guides = guidesData.map((g) => {
return {
value: g.slug,
Expand All @@ -44,22 +47,32 @@ export const ObjectiveDetails = ({
return (
objectiveContData.title.trim() === "" ||
objectiveContData.summary.trim() === "" ||
objectiveContData.subjects.length === 0 ||
objectiveContData.targets.length === 0 ||
!objectiveContData.featured
);
}, [
objectiveContData.title,
objectiveContData.summary,
objectiveContData.subjects,
objectiveContData.targets,
objectiveContData.featured,
]);

const handleNextClick = (e: React.MouseEvent) => {
if (isNextDisabled) {
e.preventDefault();
setShowErrors(true);
toast.error("Please fill out all required fields.");
}
};

return (
<Stepper.Content step="objective-details">
<StepperActionHeader
title={"Objective Details"}
Stepper={Stepper}
nextDisabled={isNextDisabled}
onNextClick={handleNextClick}
/>

<FieldGroup>
Expand All @@ -79,7 +92,11 @@ export const ObjectiveDetails = ({
autoComplete="Title"
maxLength={50}
placeholder="Choose a title. (Maximum 50 characters)."
className="h-10 rounded-md"
className={`h-10 rounded-md ${
showErrors && !objectiveContData.title.trim()
? "border-destructive focus-visible:ring-destructive/20"
: ""
}`}
required
value={objectiveContData.title}
onChange={(e) =>
Expand All @@ -103,7 +120,11 @@ export const ObjectiveDetails = ({
</div>

<Textarea
className="h-32 w-full min-w-0 resize-none"
className={`h-32 w-full min-w-0 resize-none ${
showErrors && !objectiveContData.summary.trim()
? "border-destructive focus-visible:ring-destructive/20"
: ""
}`}
rows={4}
maxLength={250}
placeholder="Write a summary for the objective."
Expand All @@ -118,6 +139,38 @@ export const ObjectiveDetails = ({
/>
</Field>

<Field className="space-y-2">
<div className="space-y-1">
<FieldLabel required className="mono-micro">
Subjects
</FieldLabel>
<FieldDescription className="text-xs">
Select at least one subject this objective falls under.
</FieldDescription>
</div>

<Combobox
multiple
className={
showErrors && objectiveContData.subjects.length === 0
? "border-destructive focus-visible:ring-destructive/20"
: ""
}
items={subjectsData.map((s) => ({
value: s.slug,
label: s.name,
description: s.summary,
}))}
value={objectiveContData.subjects}
onValueChange={(subjects) =>
setObjectiveContData((prev) => ({
...prev,
subjects,
}))
}
/>
</Field>

<Field className="space-y-2">
<div className="space-y-1">
<FieldLabel required className="mono-micro">
Expand All @@ -131,6 +184,11 @@ export const ObjectiveDetails = ({

<Combobox
multiple
className={
showErrors && objectiveContData.targets.length === 0
? "border-destructive focus-visible:ring-destructive/20"
: ""
}
items={guides}
value={objectiveContData.targets}
onValueChange={(targets) => {
Expand Down Expand Up @@ -166,6 +224,11 @@ export const ObjectiveDetails = ({

<Combobox
disabled={targs.length === 0}
className={
showErrors && !objectiveContData.featured
? "border-destructive focus-visible:ring-destructive/20"
: ""
}
items={targs}
value={objectiveContData.featured}
onValueChange={(featured) =>
Expand Down
32 changes: 29 additions & 3 deletions app/src/components/contribute/steps/OrderObjectiveGuides.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
User,
Workflow,
} from "lucide-react";
import { toast } from "sonner";
import { GuideGraph } from "../graph-view/GuideGraph";
import type { Dispatch, SetStateAction } from "react";
import type { ObjectiveContribution } from "@/types/contributions";
Expand Down Expand Up @@ -106,6 +107,7 @@ export const OrderObjectiveGuides = ({
objectiveContData,
setObjectiveContData,
}: PropTypes) => {
const [showErrors, setShowErrors] = useState(false);
const [targetSlug, setTargetSlug] = useState<string>(
objectiveContData.targets[0] || ""
);
Expand Down Expand Up @@ -262,12 +264,29 @@ export const OrderObjectiveGuides = ({
updateSubObjective(targetSlug, newSeq);
};

const isNextDisabled =
objectiveContData.subObjectives.length < objectiveContData.targets.length;

const handleNextClick = (e: React.MouseEvent) => {
if (isNextDisabled) {
e.preventDefault();
setShowErrors(true);
toast.error(
"Please visit and order every target guide before proceeding."
);
}
};

return (
<Stepper.Content
step="objective-ordering"
className="flex min-h-0 w-full flex-1 flex-col"
>
<StepperActionHeader title={"Order Guides"} Stepper={Stepper} />
<StepperActionHeader
title={"Order Guides"}
Stepper={Stepper}
onNextClick={handleNextClick}
/>

<FieldGroup className="mt-0 flex min-h-0 flex-1 flex-col">
{/* Target Guide Sequence */}
Expand All @@ -287,6 +306,9 @@ export const OrderObjectiveGuides = ({
if (!guide) return null;

const isActive = slug === targetSlug;
const hasVisited = objectiveContData.subObjectives.some(
(s) => s.targetSlug === slug
);

return (
<div key={slug} className="flex shrink-0 items-center gap-2">
Expand All @@ -297,14 +319,18 @@ export const OrderObjectiveGuides = ({
className={`flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors ${
isActive
? "border-primary bg-primary/10 text-primary ring-1 ring-primary/20"
: "border-border bg-background text-muted-foreground hover:border-primary/50 hover:bg-muted"
: showErrors && !hasVisited
? "border-destructive bg-destructive/5 text-destructive hover:bg-destructive/10"
: "border-border bg-background text-muted-foreground hover:border-primary/50 hover:bg-muted"
}`}
>
<span
className={`flex h-5 w-5 items-center justify-center rounded-full text-xs ${
isActive
? "bg-primary text-primary-foreground"
: "bg-muted-foreground/20 text-muted-foreground"
: showErrors && !hasVisited
? "bg-destructive/20 text-destructive"
: "bg-muted-foreground/20 text-muted-foreground"
}`}
>
{index + 1}
Expand Down
Loading
Loading