mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 23:48:26 +00:00
Feat: add onboarding screens
This commit is contained in:
parent
24038a4789
commit
b02c03390a
@ -1,18 +1,30 @@
|
|||||||
import { Button } from "../ui";
|
import { Button } from "../ui";
|
||||||
import Logo from "../logo/logo";
|
import Logo from "../logo/logo";
|
||||||
import { Theme, useTheme } from "remix-themes";
|
import { Theme, useTheme } from "remix-themes";
|
||||||
|
import { GalleryVerticalEnd } from "lucide-react";
|
||||||
|
|
||||||
export function LoginPageLayout({ children }: { children: React.ReactNode }) {
|
export function LoginPageLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
<div className="grid min-h-svh lg:grid-cols-2">
|
||||||
<div className="flex w-full max-w-sm flex-col items-center gap-2">
|
<div className="flex flex-col gap-4 p-6 md:p-10">
|
||||||
<div className="flex size-10 items-center justify-center rounded-md">
|
<div className="flex justify-center gap-2 md:justify-start">
|
||||||
<Logo width={60} height={60} />
|
<a href="#" className="flex items-center gap-2 font-medium">
|
||||||
|
<div className="flex size-8 items-center justify-center rounded-md">
|
||||||
|
<Logo width={60} height={60} />
|
||||||
|
</div>
|
||||||
|
C.O.R.E.
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" className="flex items-center gap-2 self-center font-medium">
|
<div className="flex flex-1 items-center justify-center">
|
||||||
<div className="font-mono">C.O.R.E.</div>
|
<div className="w-full max-w-sm">{children}</div>
|
||||||
</a>
|
</div>
|
||||||
{children}
|
</div>
|
||||||
|
<div className="relative hidden lg:block">
|
||||||
|
<img
|
||||||
|
src="/login.png"
|
||||||
|
alt="Image"
|
||||||
|
className="absolute inset-0 h-full w-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
26
apps/webapp/app/components/layout/onboarding-layout.tsx
Normal file
26
apps/webapp/app/components/layout/onboarding-layout.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Button } from "../ui";
|
||||||
|
import Logo from "../logo/logo";
|
||||||
|
import { Theme, useTheme } from "remix-themes";
|
||||||
|
|
||||||
|
export function LoginPageLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10"
|
||||||
|
style={{
|
||||||
|
backgroundImage: 'url("/back.png")',
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex w-full max-w-sm flex-col items-center gap-2">
|
||||||
|
<div className="flex size-10 items-center justify-center rounded-md">
|
||||||
|
<Logo width={60} height={60} />
|
||||||
|
</div>
|
||||||
|
<a href="#" className="flex items-center gap-2 self-center font-medium">
|
||||||
|
<div className="font-mono">C.O.R.E.</div>
|
||||||
|
</a>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -39,7 +39,7 @@ export function NavUser({ user }: { user: ExtendedUser }) {
|
|||||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate font-medium">
|
<span className="truncate font-medium">
|
||||||
Harshith Mullapudi
|
{user.displayName}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground truncate text-xs">
|
<span className="text-muted-foreground truncate text-xs">
|
||||||
{user.email}
|
{user.email}
|
||||||
|
|||||||
@ -200,22 +200,20 @@ export async function getUserByEmail(email: User["email"]) {
|
|||||||
|
|
||||||
export function updateUser({
|
export function updateUser({
|
||||||
id,
|
id,
|
||||||
name,
|
|
||||||
email,
|
|
||||||
marketingEmails,
|
marketingEmails,
|
||||||
referralSource,
|
referralSource,
|
||||||
}: Pick<User, "id" | "name" | "email"> & {
|
onboardingComplete,
|
||||||
|
}: Pick<User, "id" | "onboardingComplete"> & {
|
||||||
marketingEmails?: boolean;
|
marketingEmails?: boolean;
|
||||||
referralSource?: string;
|
referralSource?: string;
|
||||||
}) {
|
}) {
|
||||||
return prisma.user.update({
|
return prisma.user.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
name,
|
|
||||||
email,
|
|
||||||
marketingEmails,
|
marketingEmails,
|
||||||
referralSource,
|
referralSource,
|
||||||
confirmedBasicDetails: true,
|
confirmedBasicDetails: true,
|
||||||
|
onboardingComplete,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,11 @@ import { redirect, type MetaFunction } from "@remix-run/node";
|
|||||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||||
|
|
||||||
import { requireUser } from "~/services/session.server";
|
import { requireUser } from "~/services/session.server";
|
||||||
import { confirmBasicDetailsPath, dashboardPath } from "~/utils/pathBuilder";
|
import {
|
||||||
|
confirmBasicDetailsPath,
|
||||||
|
dashboardPath,
|
||||||
|
onboardingPath,
|
||||||
|
} from "~/utils/pathBuilder";
|
||||||
|
|
||||||
export const meta: MetaFunction = () => {
|
export const meta: MetaFunction = () => {
|
||||||
return [
|
return [
|
||||||
@ -17,6 +21,8 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||||||
//you have to confirm basic details before you can do anything
|
//you have to confirm basic details before you can do anything
|
||||||
if (!user.confirmedBasicDetails) {
|
if (!user.confirmedBasicDetails) {
|
||||||
return redirect(confirmBasicDetailsPath());
|
return redirect(confirmBasicDetailsPath());
|
||||||
|
} else if (!user.onboardingComplete) {
|
||||||
|
return redirect(onboardingPath());
|
||||||
} else {
|
} else {
|
||||||
return redirect(dashboardPath());
|
return redirect(dashboardPath());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { json } from "@remix-run/node";
|
import { json } from "@remix-run/node";
|
||||||
|
|
||||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
import { createHybridActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||||
import { addToQueue } from "~/lib/ingest.server";
|
import { addToQueue } from "~/lib/ingest.server";
|
||||||
import { IngestBodyRequest } from "~/trigger/ingest/ingest";
|
import { IngestBodyRequest } from "~/trigger/ingest/ingest";
|
||||||
|
|
||||||
const { action, loader } = createActionApiRoute(
|
const { action, loader } = createHybridActionApiRoute(
|
||||||
{
|
{
|
||||||
body: IngestBodyRequest,
|
body: IngestBodyRequest,
|
||||||
allowJWT: true,
|
allowJWT: true,
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useActionData } from "@remix-run/react";
|
import { useActionData } from "@remix-run/react";
|
||||||
import { type ActionFunctionArgs, json } from "@remix-run/node";
|
import {
|
||||||
|
type ActionFunctionArgs,
|
||||||
|
json,
|
||||||
|
LoaderFunctionArgs,
|
||||||
|
} from "@remix-run/node";
|
||||||
import { useForm } from "@conform-to/react";
|
import { useForm } from "@conform-to/react";
|
||||||
import { getFieldsetConstraint, parse } from "@conform-to/zod";
|
import { getFieldsetConstraint, parse } from "@conform-to/zod";
|
||||||
import { LoginPageLayout } from "~/components/layout/login-page-layout";
|
import { LoginPageLayout } from "~/components/layout/login-page-layout";
|
||||||
@ -14,10 +18,11 @@ import {
|
|||||||
import { Button } from "~/components/ui";
|
import { Button } from "~/components/ui";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { requireUserId } from "~/services/session.server";
|
import { requireUser, requireUserId } from "~/services/session.server";
|
||||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||||
import { rootPath } from "~/utils/pathBuilder";
|
import { rootPath } from "~/utils/pathBuilder";
|
||||||
import { createWorkspace } from "~/models/workspace.server";
|
import { createWorkspace } from "~/models/workspace.server";
|
||||||
|
import { typedjson } from "remix-typedjson";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
workspaceName: z
|
workspaceName: z
|
||||||
@ -55,6 +60,14 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
|
const user = await requireUser(request);
|
||||||
|
|
||||||
|
return typedjson({
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default function ConfirmBasicDetails() {
|
export default function ConfirmBasicDetails() {
|
||||||
const lastSubmission = useActionData<typeof action>();
|
const lastSubmission = useActionData<typeof action>();
|
||||||
|
|
||||||
|
|||||||
@ -8,22 +8,31 @@ import { clearRedirectTo, commitSession } from "~/services/redirectTo.server";
|
|||||||
import { AppSidebar } from "~/components/sidebar/app-sidebar";
|
import { AppSidebar } from "~/components/sidebar/app-sidebar";
|
||||||
import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
|
import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
|
||||||
import { FloatingIngestionStatus } from "~/components/ingestion/floating-ingestion-status";
|
import { FloatingIngestionStatus } from "~/components/ingestion/floating-ingestion-status";
|
||||||
|
import { redirect } from "@remix-run/node";
|
||||||
|
import { confirmBasicDetailsPath, onboardingPath } from "~/utils/pathBuilder";
|
||||||
|
|
||||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const user = await requireUser(request);
|
const user = await requireUser(request);
|
||||||
const workspace = await requireWorkpace(request);
|
const workspace = await requireWorkpace(request);
|
||||||
|
|
||||||
return typedjson(
|
//you have to confirm basic details before you can do anything
|
||||||
{
|
if (!user.confirmedBasicDetails) {
|
||||||
user,
|
return redirect(confirmBasicDetailsPath());
|
||||||
workspace,
|
} else if (!user.onboardingComplete) {
|
||||||
},
|
return redirect(onboardingPath());
|
||||||
{
|
} else {
|
||||||
headers: {
|
return typedjson(
|
||||||
"Set-Cookie": await commitSession(await clearRedirectTo(request)),
|
{
|
||||||
|
user,
|
||||||
|
workspace,
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
);
|
headers: {
|
||||||
|
"Set-Cookie": await commitSession(await clearRedirectTo(request)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
|||||||
@ -57,10 +57,12 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginPageLayout>
|
<LoginPageLayout>
|
||||||
<Card className="min-w-[300px] rounded-md p-3">
|
<Card className="w-full max-w-[350px] rounded-md bg-transparent p-3">
|
||||||
<CardHeader className="flex flex-col items-start">
|
<CardHeader className="flex flex-col items-start">
|
||||||
<CardTitle>Login to your account</CardTitle>
|
<CardTitle className="text-xl">Welcome back</CardTitle>
|
||||||
<CardDescription>Create an account or login</CardDescription>
|
<CardDescription className="text-md">
|
||||||
|
Create an account or login
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="pt-2">
|
<CardContent className="pt-2">
|
||||||
@ -69,7 +71,7 @@ export default function LoginPage() {
|
|||||||
{data.showGoogleAuth && (
|
{data.showGoogleAuth && (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="xl"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="rounded-lg text-base"
|
className="rounded-lg text-base"
|
||||||
data-action="continue with google"
|
data-action="continue with google"
|
||||||
@ -83,7 +85,7 @@ export default function LoginPage() {
|
|||||||
{data.emailLoginEnabled && (
|
{data.emailLoginEnabled && (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="xl"
|
||||||
data-action="continue with email"
|
data-action="continue with email"
|
||||||
className="text-text-bright"
|
className="text-text-bright"
|
||||||
onClick={() => (window.location.href = "/login/magic")}
|
onClick={() => (window.location.href = "/login/magic")}
|
||||||
|
|||||||
@ -13,14 +13,13 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "~/components/ui/card";
|
} from "~/components/ui/card";
|
||||||
import { Form, useNavigation } from "@remix-run/react";
|
import { Form, useNavigation } from "@remix-run/react";
|
||||||
import { Inbox, Loader, LoaderCircle, Mail } from "lucide-react";
|
import { LoaderCircle, Mail } from "lucide-react";
|
||||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { LoginPageLayout } from "~/components/layout/login-page-layout";
|
import { LoginPageLayout } from "~/components/layout/login-page-layout";
|
||||||
import { Button } from "~/components/ui";
|
import { Button } from "~/components/ui";
|
||||||
import { Fieldset } from "~/components/ui/Fieldset";
|
import { Fieldset } from "~/components/ui/Fieldset";
|
||||||
import { FormButtons } from "~/components/ui/FormButtons";
|
import { FormButtons } from "~/components/ui/FormButtons";
|
||||||
import { Header1 } from "~/components/ui/Headers";
|
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Paragraph } from "~/components/ui/Paragraph";
|
import { Paragraph } from "~/components/ui/Paragraph";
|
||||||
import { Cookie } from "@mjackson/headers";
|
import { Cookie } from "@mjackson/headers";
|
||||||
@ -145,12 +144,12 @@ export default function LoginMagicLinkPage() {
|
|||||||
<Form method="post">
|
<Form method="post">
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
{data.magicLinkSent ? (
|
{data.magicLinkSent ? (
|
||||||
<Card className="min-w-[400px] rounded-md p-3">
|
<Card className="min-w-[400px] rounded-md bg-transparent p-3">
|
||||||
<CardHeader className="flex flex-col items-start">
|
<CardHeader className="flex flex-col items-start">
|
||||||
<CardTitle className="mb-0 text-lg">
|
<CardTitle className="mb-0 text-xl">
|
||||||
Check your magic link
|
Check your magic link
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="text-md">
|
||||||
The magic link is printed in the container logs if you are
|
The magic link is printed in the container logs if you are
|
||||||
using Docker, otherwise check your server logs.
|
using Docker, otherwise check your server logs.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@ -164,6 +163,7 @@ export default function LoginMagicLinkPage() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
name="action"
|
name="action"
|
||||||
value="reset"
|
value="reset"
|
||||||
|
size="lg"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
data-action="re-enter email"
|
data-action="re-enter email"
|
||||||
>
|
>
|
||||||
@ -174,14 +174,14 @@ export default function LoginMagicLinkPage() {
|
|||||||
</Fieldset>
|
</Fieldset>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card className="min-w-[400px] rounded-md p-3">
|
<Card className="w-full max-w-[350px] rounded-md bg-transparent p-3">
|
||||||
<CardHeader className="flex flex-col items-start">
|
<CardHeader className="flex flex-col items-start">
|
||||||
<CardTitle className="mb-0 text-lg">Welcome</CardTitle>
|
<CardTitle className="text-xl">Welcome back</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="text-md">
|
||||||
Create an account or login using email
|
Create an account or login using email
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-2">
|
<CardContent className="pt-2 pl-2">
|
||||||
<Fieldset className="flex w-full flex-col items-center gap-y-2">
|
<Fieldset className="flex w-full flex-col items-center gap-y-2">
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
@ -193,28 +193,32 @@ export default function LoginMagicLinkPage() {
|
|||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<div className="flex w-full">
|
||||||
name="action"
|
<Button
|
||||||
value="send"
|
name="action"
|
||||||
type="submit"
|
value="send"
|
||||||
variant="secondary"
|
type="submit"
|
||||||
size="lg"
|
variant="secondary"
|
||||||
disabled={isLoading}
|
full
|
||||||
data-action="send a magic link"
|
size="xl"
|
||||||
>
|
className="w-full"
|
||||||
{isLoading ? (
|
disabled={isLoading}
|
||||||
<LoaderCircle className="text-primary h-4 w-4 animate-spin" />
|
data-action="send a magic link"
|
||||||
) : (
|
>
|
||||||
<Mail className="text-text-bright mr-2 size-5" />
|
{isLoading ? (
|
||||||
)}
|
<LoaderCircle className="text-primary h-4 w-4 animate-spin" />
|
||||||
{isLoading ? (
|
) : (
|
||||||
<span className="text-text-bright">Sending…</span>
|
<Mail className="text-text-bright mr-2 size-5" />
|
||||||
) : (
|
)}
|
||||||
<span className="text-text-bright">
|
{isLoading ? (
|
||||||
Send a magic link
|
<span className="text-text-bright">Sending…</span>
|
||||||
</span>
|
) : (
|
||||||
)}
|
<span className="text-text-bright">
|
||||||
</Button>
|
Send a magic link
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
{data.magicLinkError && <>{data.magicLinkError}</>}
|
{data.magicLinkError && <>{data.magicLinkError}</>}
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
230
apps/webapp/app/routes/onboarding.tsx
Normal file
230
apps/webapp/app/routes/onboarding.tsx
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { useLoaderData, useActionData, useNavigate } from "@remix-run/react";
|
||||||
|
import {
|
||||||
|
type ActionFunctionArgs,
|
||||||
|
json,
|
||||||
|
type LoaderFunctionArgs,
|
||||||
|
redirect,
|
||||||
|
createCookie,
|
||||||
|
} from "@remix-run/node";
|
||||||
|
import { useForm } from "@conform-to/react";
|
||||||
|
import { getFieldsetConstraint, parse } from "@conform-to/zod";
|
||||||
|
import { LoginPageLayout } from "~/components/layout/login-page-layout";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "~/components/ui/card";
|
||||||
|
import { Button } from "~/components/ui";
|
||||||
|
import { Textarea } from "~/components/ui/textarea";
|
||||||
|
import { Input } from "~/components/ui/input";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { requireUserId } from "~/services/session.server";
|
||||||
|
import { updateUser } from "~/models/user.server";
|
||||||
|
import { Copy, Check } from "lucide-react";
|
||||||
|
import { addToQueue } from "~/lib/ingest.server";
|
||||||
|
|
||||||
|
const ONBOARDING_STEP_COOKIE = "onboardingStep";
|
||||||
|
const onboardingStepCookie = createCookie(ONBOARDING_STEP_COOKIE, {
|
||||||
|
path: "/",
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: 60 * 60 * 24 * 7, // 1 week
|
||||||
|
});
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
aboutUser: z
|
||||||
|
.string()
|
||||||
|
.min(
|
||||||
|
10,
|
||||||
|
"Please tell us a bit more about yourself (at least 10 characters)",
|
||||||
|
)
|
||||||
|
.max(1000, "Please keep it under 1000 characters"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
|
await requireUserId(request);
|
||||||
|
|
||||||
|
// Read step from cookie
|
||||||
|
const cookieHeader = request.headers.get("Cookie");
|
||||||
|
const cookie = (await onboardingStepCookie.parse(cookieHeader)) || {};
|
||||||
|
const step = cookie.step || null;
|
||||||
|
|
||||||
|
return json({ step });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function action({ request }: ActionFunctionArgs) {
|
||||||
|
const userId = await requireUserId(request);
|
||||||
|
|
||||||
|
const formData = await request.formData();
|
||||||
|
const submission = parse(formData, { schema });
|
||||||
|
|
||||||
|
if (!submission.value || submission.intent !== "submit") {
|
||||||
|
return json(submission);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { aboutUser } = submission.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ingest memory via API call
|
||||||
|
const memoryResponse = await addToQueue(
|
||||||
|
{
|
||||||
|
source: "Core",
|
||||||
|
episodeBody: aboutUser,
|
||||||
|
referenceTime: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!memoryResponse.id) {
|
||||||
|
throw new Error("Failed to save memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user's onboarding status
|
||||||
|
await updateUser({
|
||||||
|
id: userId,
|
||||||
|
onboardingComplete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set step in cookie and redirect to GET (PRG pattern)
|
||||||
|
const cookie = await onboardingStepCookie.serialize({
|
||||||
|
step: "memory-link",
|
||||||
|
});
|
||||||
|
return redirect("/onboarding", {
|
||||||
|
headers: {
|
||||||
|
"Set-Cookie": cookie,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
return json({ errors: { body: e.message } }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Onboarding() {
|
||||||
|
const loaderData = useLoaderData<{ step: string | null }>();
|
||||||
|
const lastSubmission = useActionData<typeof action>();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const [form, fields] = useForm({
|
||||||
|
lastSubmission: lastSubmission as any,
|
||||||
|
constraint: getFieldsetConstraint(schema),
|
||||||
|
onValidate({ formData }) {
|
||||||
|
return parse(formData, { schema });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const memoryUrl = "https://core.heysol.ai/api/v1/mcp/memory";
|
||||||
|
|
||||||
|
const copyToClipboard = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(memoryUrl);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show memory link step after successful submission (step persisted in cookie)
|
||||||
|
if (loaderData.step === "memory-link") {
|
||||||
|
return (
|
||||||
|
<LoginPageLayout>
|
||||||
|
<Card className="min-w-[400px] rounded-lg bg-transparent p-3 pt-1">
|
||||||
|
<CardHeader className="flex flex-col items-start px-0">
|
||||||
|
<CardTitle className="px-0 text-xl">Your Memory Link</CardTitle>
|
||||||
|
<CardDescription className="text-md">
|
||||||
|
Here's your personal memory API endpoint. Copy this URL to connect
|
||||||
|
with external tools (Claude, Cursor etc).
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="pt-2 text-base">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div className="bg-background-3 flex items-center rounded">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="memoryUrl"
|
||||||
|
value={memoryUrl}
|
||||||
|
readOnly
|
||||||
|
className="bg-background-3 block w-full text-base"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
className="px-3"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
size="xl"
|
||||||
|
className="w-full rounded-lg px-4 py-2"
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
>
|
||||||
|
Continue to Dashboard
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</LoginPageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoginPageLayout>
|
||||||
|
<Card className="bg-background-2 w-full max-w-[400px] rounded-lg p-3 pt-1">
|
||||||
|
<CardHeader className="flex flex-col items-start px-0"></CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="text-base">
|
||||||
|
<form method="post" {...form.props}>
|
||||||
|
<div className="space-y-4 pl-1">
|
||||||
|
<CardTitle className="text-md mb-0 -ml-1 px-0 text-xl">
|
||||||
|
Tell me about you
|
||||||
|
</CardTitle>
|
||||||
|
<div>
|
||||||
|
<Textarea
|
||||||
|
id="aboutUser"
|
||||||
|
placeholder="I'm Steve Jobs, co-founder of Apple. I helped create the iPhone, iPad, and Mac. I'm passionate about design, technology, and making products that change the world. I spent much of my life in California, working on innovative devices and inspiring creativity. I enjoy simplicity, calligraphy, and thinking differently..."
|
||||||
|
name={fields.aboutUser.name}
|
||||||
|
className="block min-h-[120px] w-full bg-transparent px-0 text-base"
|
||||||
|
rows={10}
|
||||||
|
/>
|
||||||
|
{fields.aboutUser.error && (
|
||||||
|
<div className="text-sm text-red-500">
|
||||||
|
{fields.aboutUser.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="secondary"
|
||||||
|
size="xl"
|
||||||
|
className="rounded-lg px-4 py-2"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</LoginPageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -61,6 +61,7 @@ export async function requireUser(request: Request) {
|
|||||||
createdAt: user.createdAt,
|
createdAt: user.createdAt,
|
||||||
updatedAt: user.updatedAt,
|
updatedAt: user.updatedAt,
|
||||||
confirmedBasicDetails: user.confirmedBasicDetails,
|
confirmedBasicDetails: user.confirmedBasicDetails,
|
||||||
|
onboardingComplete: user.onboardingComplete,
|
||||||
isImpersonating: !!impersonationId,
|
isImpersonating: !!impersonationId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -285,6 +285,13 @@ async function handleMessageResponse(
|
|||||||
messageTypes: messages.map((m) => m.type),
|
messageTypes: messages.map((m) => m.type),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const responses = {
|
||||||
|
activities: [],
|
||||||
|
state: undefined,
|
||||||
|
account: undefined,
|
||||||
|
unhandled: [],
|
||||||
|
} as any;
|
||||||
|
|
||||||
// Group messages by type
|
// Group messages by type
|
||||||
const grouped: Record<string, Message[]> = {};
|
const grouped: Record<string, Message[]> = {};
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
@ -296,48 +303,54 @@ async function handleMessageResponse(
|
|||||||
|
|
||||||
// Handle "activity" messages
|
// Handle "activity" messages
|
||||||
if (grouped["activity"]) {
|
if (grouped["activity"]) {
|
||||||
await handleActivityMessage(
|
const activities = await handleActivityMessage(
|
||||||
grouped["activity"],
|
grouped["activity"],
|
||||||
integrationAccountId as string,
|
integrationAccountId as string,
|
||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
responses.activities = activities;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle "state" messages
|
// Handle "state" messages
|
||||||
if (grouped["state"]) {
|
if (grouped["state"]) {
|
||||||
await handleStateMessage(
|
const state = await handleStateMessage(
|
||||||
grouped["state"],
|
grouped["state"],
|
||||||
integrationAccountId as string,
|
integrationAccountId as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
responses.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle "identifier" messages
|
// Handle "identifier" messages
|
||||||
if (grouped["identifier"]) {
|
if (grouped["identifier"]) {
|
||||||
await handleIdentifierMessage(grouped["identifier"][0]);
|
const identifier = await handleIdentifierMessage(
|
||||||
|
grouped["identifier"][0],
|
||||||
|
);
|
||||||
|
return identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle "account" messages (these may involve Prisma writes)
|
// Handle "account" messages (these may involve Prisma writes)
|
||||||
if (grouped["account"]) {
|
if (grouped["account"]) {
|
||||||
await handleAccountMessage(
|
const account = await handleAccountMessage(
|
||||||
grouped["account"],
|
grouped["account"],
|
||||||
integrationDefinition,
|
integrationDefinition,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
userId,
|
userId,
|
||||||
integrationAccountId as string,
|
integrationAccountId as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
responses.account = account;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn for unknown message types
|
// Warn for unknown message types
|
||||||
for (const type of Object.keys(grouped)) {
|
for (const type of Object.keys(grouped)) {
|
||||||
if (!["activity", "state", "identifier", "account"].includes(type)) {
|
if (!["activity", "state", "identifier", "account"].includes(type)) {
|
||||||
for (const message of grouped[type]) {
|
responses.unhandled.push(grouped[type]);
|
||||||
logger.warn("Unknown message type", {
|
|
||||||
messageType: type,
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return responses;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to handle CLI message response", {
|
logger.error("Failed to handle CLI message response", {
|
||||||
error: error instanceof Error ? error.message : "Unknown error",
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export type AuthenticatedUser = {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
confirmedBasicDetails: boolean;
|
confirmedBasicDetails: boolean;
|
||||||
|
onboardingComplete: boolean;
|
||||||
authMethod: 'session' | 'pat' | 'oauth2';
|
authMethod: 'session' | 'pat' | 'oauth2';
|
||||||
oauth2?: {
|
oauth2?: {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
@ -47,6 +48,7 @@ export async function requireAuth(request: Request): Promise<AuthenticatedUser>
|
|||||||
createdAt: user.createdAt,
|
createdAt: user.createdAt,
|
||||||
updatedAt: user.updatedAt,
|
updatedAt: user.updatedAt,
|
||||||
confirmedBasicDetails: user.confirmedBasicDetails,
|
confirmedBasicDetails: user.confirmedBasicDetails,
|
||||||
|
onboardingComplete: user.onboardingComplete,
|
||||||
authMethod: 'pat',
|
authMethod: 'pat',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -65,6 +67,7 @@ export async function requireAuth(request: Request): Promise<AuthenticatedUser>
|
|||||||
createdAt: accessToken.user.createdAt,
|
createdAt: accessToken.user.createdAt,
|
||||||
updatedAt: accessToken.user.updatedAt,
|
updatedAt: accessToken.user.updatedAt,
|
||||||
confirmedBasicDetails: accessToken.user.confirmedBasicDetails,
|
confirmedBasicDetails: accessToken.user.confirmedBasicDetails,
|
||||||
|
onboardingComplete: accessToken.user.onboardingComplete,
|
||||||
authMethod: 'oauth2',
|
authMethod: 'oauth2',
|
||||||
oauth2: {
|
oauth2: {
|
||||||
clientId: accessToken.client.clientId,
|
clientId: accessToken.client.clientId,
|
||||||
@ -89,6 +92,7 @@ export async function requireAuth(request: Request): Promise<AuthenticatedUser>
|
|||||||
createdAt: sessionUser.createdAt,
|
createdAt: sessionUser.createdAt,
|
||||||
updatedAt: sessionUser.updatedAt,
|
updatedAt: sessionUser.updatedAt,
|
||||||
confirmedBasicDetails: sessionUser.confirmedBasicDetails,
|
confirmedBasicDetails: sessionUser.confirmedBasicDetails,
|
||||||
|
onboardingComplete: sessionUser.onboardingComplete,
|
||||||
authMethod: 'session',
|
authMethod: 'session',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,10 @@ export function confirmBasicDetailsPath() {
|
|||||||
return `/confirm-basic-details`;
|
return `/confirm-basic-details`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function onboardingPath() {
|
||||||
|
return `/onboarding`;
|
||||||
|
}
|
||||||
|
|
||||||
export function homePath() {
|
export function homePath() {
|
||||||
return `/home`;
|
return `/home`;
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
apps/webapp/public/login.png
Normal file
BIN
apps/webapp/public/login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.0 MiB |
@ -186,7 +186,6 @@ export async function initCommand() {
|
|||||||
const parsed = parse(file);
|
const parsed = parse(file);
|
||||||
const envVarsExpand = expand({ parsed, processEnv: {} }).parsed || {};
|
const envVarsExpand = expand({ parsed, processEnv: {} }).parsed || {};
|
||||||
|
|
||||||
console.log(envVarsExpand);
|
|
||||||
await executeCommandInteractive("docker compose up -d", {
|
await executeCommandInteractive("docker compose up -d", {
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
message: "Starting Core services with new Trigger.dev configuration...",
|
message: "Starting Core services with new Trigger.dev configuration...",
|
||||||
|
|||||||
@ -10,8 +10,6 @@ export interface CommandOptions {
|
|||||||
|
|
||||||
export function executeCommandInteractive(command: string, options: CommandOptions): Promise<void> {
|
export function executeCommandInteractive(command: string, options: CommandOptions): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
console.log(process.env);
|
|
||||||
|
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
s.start(options.message);
|
s.start(options.message);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "onboardingComplete" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@ -482,6 +482,7 @@ model User {
|
|||||||
|
|
||||||
marketingEmails Boolean @default(true)
|
marketingEmails Boolean @default(true)
|
||||||
confirmedBasicDetails Boolean @default(false)
|
confirmedBasicDetails Boolean @default(false)
|
||||||
|
onboardingComplete Boolean @default(false)
|
||||||
|
|
||||||
referralSource String?
|
referralSource String?
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@redplanethq/sdk",
|
"name": "@redplanethq/sdk",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"description": "CORE Node.JS SDK",
|
"description": "CORE Node.JS SDK",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
|
|||||||
@ -78,7 +78,7 @@ export abstract class IntegrationCLI {
|
|||||||
|
|
||||||
const messages: Message[] = await this.handleEvent({
|
const messages: Message[] = await this.handleEvent({
|
||||||
event: IntegrationEventType.PROCESS,
|
event: IntegrationEventType.PROCESS,
|
||||||
eventBody: { eventData },
|
eventBody: eventData,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user