Skip to content

Commit 0e448fc

Browse files
feat: onboarding
1 parent 85fab42 commit 0e448fc

File tree

3 files changed

+236
-63
lines changed

3 files changed

+236
-63
lines changed

templates/react-ts-example/src/App.tsx

+44-37
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { initSatellite } from "@junobuild/core";
2-
import { FC, useEffect } from "react";
2+
import { FC, useEffect, useState } from "react";
3+
import { Onboarding } from "./components/Onboarding";
34
import { Auth } from "./components/Auth";
5+
import { Table } from "./components/Table";
6+
import { Modal } from "./components/Modal";
47
import { Background } from "./components/Background";
58
import { Footer } from "./components/Footer";
6-
import { Modal } from "./components/Modal";
7-
import { Table } from "./components/Table";
8-
import { Banner } from "./components/Banner.tsx";
99

1010
const App: FC = () => {
11+
const [onboardingCompleted, setOnboardingCompleted] = useState(false);
12+
1113
useEffect(() => {
1214
(async () =>
1315
await initSatellite({
@@ -18,39 +20,44 @@ const App: FC = () => {
1820
}, []);
1921

2022
return (
21-
<>
22-
<div className="relative isolate min-h-[100dvh]">
23-
<Banner />
24-
25-
<main className="mx-auto max-w-(--breakpoint-2xl) px-8 py-16 md:px-24 [@media(min-height:800px)]:min-h-[calc(100dvh-128px)]">
26-
<h1 className="text-5xl font-bold tracking-tight md:pt-24 md:text-6xl dark:text-white">
27-
Example App
28-
</h1>
29-
<p className="py-4 md:max-w-lg dark:text-white">
30-
Explore this demo app built with React, Tailwind, and{" "}
31-
<a
32-
href="https://juno.build"
33-
rel="noopener noreferrer"
34-
target="_blank"
35-
className="underline"
36-
>
37-
Juno
38-
</a>
39-
, showcasing a practical application of these technologies.
40-
</p>
41-
42-
<Auth>
43-
<Table />
44-
45-
<Modal />
46-
</Auth>
47-
</main>
48-
49-
<Footer />
50-
51-
<Background />
52-
</div>
53-
</>
23+
<div className="relative isolate min-h-[100dvh]">
24+
<main className="mx-auto max-w-(--breakpoint-2xl) px-8 py-16 md:px-24 [@media(min-height:800px)]:min-h-[calc(100dvh-128px)]">
25+
<h1 className="text-5xl font-bold tracking-tight md:pt-24 md:text-6xl dark:text-white">
26+
Example App
27+
</h1>
28+
29+
<Onboarding
30+
onComplete={(completed: boolean) => setOnboardingCompleted(completed)}
31+
/>
32+
33+
{(onboardingCompleted || !import.meta.env.DEV) && (
34+
<>
35+
<p className="py-4 md:max-w-lg dark:text-white">
36+
Sign-in to explore this demo app built with React, Tailwind, and{" "}
37+
<a
38+
href="https://juno.build"
39+
rel="noopener noreferrer"
40+
target="_blank"
41+
className="underline"
42+
>
43+
Juno
44+
</a>
45+
.
46+
</p>
47+
48+
<Auth>
49+
<Table />
50+
51+
<Modal />
52+
</Auth>
53+
</>
54+
)}
55+
</main>
56+
57+
<Footer />
58+
59+
<Background />
60+
</div>
5461
);
5562
};
5663

templates/react-ts-example/src/components/Banner.tsx

-26
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { FC, useEffect, useState } from "react";
2+
3+
type StepId = "satellite" | "config" | "datastore" | "storage";
4+
5+
type StepCompleted = boolean;
6+
7+
interface UiStepAction {
8+
description: string;
9+
label: string;
10+
url: string;
11+
}
12+
13+
interface UiStep {
14+
description: string;
15+
action?: UiStepAction;
16+
status: "todo" | "pending" | "completed";
17+
}
18+
19+
interface OnboardingProps {
20+
onComplete: (completed: boolean) => void;
21+
}
22+
23+
export const Onboarding: FC<OnboardingProps> = ({ onComplete }) => {
24+
const [uiSteps, setUiSteps] = useState<Record<StepId, UiStep>>({
25+
satellite: {
26+
description: "Your project needs a Satellite for local dev.",
27+
action: {
28+
description: "Create one now!",
29+
label: "Open the Juno Console to create a new Satellite for testing",
30+
url: "http://localhost:5866",
31+
},
32+
status: "todo",
33+
},
34+
config: {
35+
description:
36+
"Set the Satellite ID in your juno.config file and restart the development server.",
37+
status: "pending",
38+
},
39+
datastore: {
40+
description: "Create a 'notes' collection for your documents.",
41+
action: {
42+
description: "Open Datastore",
43+
label: "Open the Datastore in the Juno Console",
44+
url: "http://localhost:5866/datastore",
45+
},
46+
status: "pending",
47+
},
48+
storage: {
49+
description: "Create an 'images' collection to store assets.",
50+
action: {
51+
description: "Open Storage",
52+
label: "Open the Storage in the Juno Console",
53+
url: "http://localhost:5866/storage",
54+
},
55+
status: "pending",
56+
},
57+
});
58+
59+
const [steps, setSteps] = useState<Record<StepId, StepCompleted>>({
60+
satellite: false,
61+
config: false,
62+
datastore: false,
63+
storage: false,
64+
});
65+
66+
const [allStepsCompleted, setAllStepsCompleted] = useState<boolean>(true);
67+
68+
const KEY = "create-juno-onboarding-steps";
69+
70+
useEffect(() => {
71+
const savedSteps = localStorage.getItem(KEY);
72+
73+
if (savedSteps === null) {
74+
setAllStepsCompleted(false);
75+
return;
76+
}
77+
78+
updateSteps(JSON.parse(savedSteps));
79+
}, []);
80+
81+
useEffect(() => {
82+
onComplete(allStepsCompleted);
83+
}, [allStepsCompleted]);
84+
85+
useEffect(() => {
86+
const updatedUiSteps: Record<StepId, UiStep> = {
87+
satellite: {
88+
...uiSteps.satellite,
89+
status: steps["satellite"] ? "completed" : "todo",
90+
},
91+
config: {
92+
...uiSteps.config,
93+
status: steps["config"]
94+
? "completed"
95+
: uiSteps.satellite.status === "completed" || steps["satellite"]
96+
? "todo"
97+
: "pending",
98+
},
99+
datastore: {
100+
...uiSteps.datastore,
101+
status: steps["datastore"]
102+
? "completed"
103+
: uiSteps.config.status === "completed" || steps["config"]
104+
? "todo"
105+
: "pending",
106+
},
107+
storage: {
108+
...uiSteps.storage,
109+
status: steps["storage"]
110+
? "completed"
111+
: uiSteps.datastore.status === "completed" || steps["datastore"]
112+
? "todo"
113+
: "pending",
114+
},
115+
};
116+
117+
setUiSteps(updatedUiSteps);
118+
}, [steps]);
119+
120+
const toggleStep = (stepId: string) => {
121+
const updatedSteps = {
122+
...steps,
123+
[stepId]: !steps[stepId as StepId],
124+
};
125+
126+
updateSteps(updatedSteps);
127+
};
128+
129+
const updateSteps = (updatedSteps: Record<StepId, StepCompleted>) => {
130+
setSteps(updatedSteps);
131+
132+
localStorage.setItem(KEY, JSON.stringify(updatedSteps));
133+
134+
const completed = Object.values(updatedSteps).every(
135+
(completed) => completed,
136+
);
137+
138+
setAllStepsCompleted(completed);
139+
};
140+
141+
if (!import.meta.env.DEV) {
142+
return null;
143+
}
144+
145+
if (allStepsCompleted) {
146+
return null;
147+
}
148+
149+
return (
150+
<div className="mt-4 w-full max-w-2xl dark:text-white">
151+
<p className="py-4 md:max-w-lg dark:text-white">
152+
To get started with your Juno project, please complete the following
153+
steps:
154+
</p>
155+
156+
<div className="flex flex-col items-start gap-2">
157+
{Object.entries(uiSteps).map(([stepId, step]) => (
158+
<label
159+
key={stepId}
160+
htmlFor={stepId}
161+
className={`flex w-full max-w-2xl items-center gap-4 rounded-sm border-[3px] border-black bg-white py-2 transition-all dark:bg-black dark:text-white ${step.status === "pending" ? "opacity-25" : ""}`}
162+
>
163+
<input
164+
type="checkbox"
165+
className="accent-screamin-green-300 disabled:accent-screamin-green-300 size-5 min-w-6 rounded shadow-sm"
166+
id={stepId}
167+
checked={steps[stepId as StepId]}
168+
onChange={() => toggleStep(stepId)}
169+
disabled={step.status === "pending"}
170+
/>
171+
172+
<div>
173+
<p>
174+
<span>{step.description}</span>
175+
{step.action !== undefined && step.status === "todo" && (
176+
<a
177+
href={step.action.url}
178+
target="_blank"
179+
className={`ml-2 inline-block px-1 ${step.status === "todo" ? "bg-screamin-green-200 rounded-sm font-bold text-black underline" : ""}`}
180+
aria-label={step.action.label}
181+
>
182+
{step.action.description}
183+
</a>
184+
)}
185+
</p>
186+
</div>
187+
</label>
188+
))}
189+
</div>
190+
</div>
191+
);
192+
};

0 commit comments

Comments
 (0)