Skip to content
Merged
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
26 changes: 21 additions & 5 deletions src/components/LoginStatusContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,29 @@
import React, { createContext, useContext, useEffect, useState } from "react";
import { LoaderCircle } from "lucide-react";

const LoginStatusContext = createContext<{
type AuthSession = {
isLoggedIn: boolean | null;
isLoading: boolean;
}>({
email?: string;
};

type SessionResponse = {
isLoggedIn?: boolean;
userInfo?: {
email?: string;
};
};

const LoginStatusContext = createContext<AuthSession>({
isLoggedIn: null,
isLoading: true,
email: undefined,
});

export function LoginStatusProvider({ children }: { children: React.ReactNode }) {
const [isLoggedIn, setIsLoggedIn] = useState<boolean | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [email, setEmail] = useState<string | undefined>(undefined);

useEffect(() => {
async function checkAuth() {
Expand All @@ -24,17 +36,21 @@ export function LoginStatusProvider({ children }: { children: React.ReactNode })

if (response && response.ok) {
try {
const session = await response.json();
const session = (await response.json()) as SessionResponse;
setIsLoggedIn(!!(session && session.isLoggedIn));
setEmail(session.userInfo?.email);
} catch (_e) {
console.error("Failed to parse session data", _e);
setIsLoggedIn(false);
setEmail(undefined);
}
} else {
setIsLoggedIn(false);
setEmail(undefined);
}
} catch (_err) {
setIsLoggedIn(false);
setEmail(undefined);
} finally {
setIsLoading(false);
}
Expand All @@ -44,13 +60,13 @@ export function LoginStatusProvider({ children }: { children: React.ReactNode })
}, []);

return (
<LoginStatusContext.Provider value={{ isLoggedIn, isLoading }}>
<LoginStatusContext.Provider value={{ isLoggedIn, isLoading, email }}>
{children}
</LoginStatusContext.Provider>
);
}

function useLoginStatus() {
export function useLoginStatus() {
return useContext(LoginStatusContext);
}

Expand Down
17 changes: 16 additions & 1 deletion src/components/feedback/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
type BlockFeedback,
type PageFeedback,
} from "./schema";
import { useLoginStatus } from "../LoginStatusContext";
import { z } from "zod/mini";

const rateButtonVariants = cva(
Expand All @@ -39,6 +40,7 @@ const rateButtonVariants = cva(

const pageFeedbackResult = z.extend(pageFeedback, {
response: actionResponse,
usedAccountEmail: z.optional(z.boolean()),
});

const blockFeedbackResult = z.extend(blockFeedback, {
Expand All @@ -53,6 +55,7 @@ export function Feedback({
}: {
onSendAction: (feedback: PageFeedback) => Promise<ActionResponse>;
}) {
const { isLoading, isLoggedIn, email } = useLoginStatus();
const { pathname: url } = useLocation();
const { previous, setPrevious } = useSubmissionStorage(url, (v) => {
const result = pageFeedbackResult.safeParse(v);
Expand All @@ -71,10 +74,12 @@ export function Feedback({
opinion,
message,
};
const usedAccountEmail = !isLoading && !!isLoggedIn && !!email;

const response = await onSendAction(feedback);
setPrevious({
response,
usedAccountEmail,
...feedback,
});
setMessage("");
Expand Down Expand Up @@ -128,7 +133,12 @@ export function Feedback({
<CollapsibleContent className="mt-3">
{previous ? (
<div className="px-3 py-6 flex flex-col items-center gap-3 bg-fd-card text-fd-muted-foreground text-sm text-center rounded-xl">
<p>Thank you for your feedback!</p>
<div className="flex flex-col gap-1">
<p>Thank you for your feedback!</p>
{previous.usedAccountEmail ? (
<p>If we need more detail, we may follow up by email.</p>
) : null}
</div>
<div className="flex flex-row items-center gap-2">
{previous.response?.githubUrl ? (
<a
Expand Down Expand Up @@ -177,6 +187,11 @@ export function Feedback({
}
}}
/>
{!isLoading && isLoggedIn && email ? (
<p className="text-sm text-fd-muted-foreground">
If we need to follow up, we may reply to your account email.
</p>
) : null}
<button
type="submit"
className={cn(buttonVariants({ color: "outline" }), "w-fit px-3")}
Expand Down
27 changes: 15 additions & 12 deletions src/routes/$.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { LLMCopyButton, ViewOptions } from "@/components/ai/page-actions";
import { SidebarDiscordLink } from "@/components/SidebarDiscordLink";
import { Feedback } from "@/components/feedback/client";
import type { ActionResponse, PageFeedback } from "@/components/feedback/schema";
import { LoginStatusProvider } from "@/components/LoginStatusContext";
import {
buildCanonicalUrl,
DEFAULT_DESCRIPTION,
Expand Down Expand Up @@ -131,18 +132,20 @@ const clientLoader = browserCollections.docs.createClientLoader({
}

return (
<DocsPage toc={toc}>
<DocsTitle>{frontmatter.title}</DocsTitle>
<DocsDescription>{frontmatter.description}</DocsDescription>
<div className="flex flex-row gap-2 items-center border-b -mt-4 pb-6">
<LLMCopyButton markdownUrl={`${url}.mdx`} />
<ViewOptions markdownUrl={`${url}.mdx`} githubUrl={githubUrl} />
</div>
<DocsBody>
<MDX components={getMDXComponents()} />
</DocsBody>
<Feedback onSendAction={onSendFeedback} />
</DocsPage>
<LoginStatusProvider>
<DocsPage toc={toc}>
<DocsTitle>{frontmatter.title}</DocsTitle>
<DocsDescription>{frontmatter.description}</DocsDescription>
<div className="flex flex-row gap-2 items-center border-b -mt-4 pb-6">
<LLMCopyButton markdownUrl={`${url}.mdx`} />
<ViewOptions markdownUrl={`${url}.mdx`} githubUrl={githubUrl} />
</div>
<DocsBody>
<MDX components={getMDXComponents()} />
</DocsBody>
<Feedback onSendAction={onSendFeedback} />
</DocsPage>
</LoginStatusProvider>
);
},
});
Expand Down
30 changes: 29 additions & 1 deletion src/routes/api/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,36 @@ type AIFeedback = {

type Feedback = DocsFeedback | AIFeedback;

type SessionResponse = {
isLoggedIn?: boolean;
userInfo?: {
email?: string;
};
};

function escapeTripleBackticks(input: string): string {
return input.replace(/```/g, "`\u200b``");
}

async function getAuthenticatedUserEmail(request: Request): Promise<string | undefined> {
const sessionUrl = new URL("/api/auth/session", request.url);
const cookie = request.headers.get("cookie");

try {
const sessionResponse = await fetch(sessionUrl, {
headers: cookie ? { cookie } : undefined,
});
if (!sessionResponse.ok) return undefined;

const session = (await sessionResponse.json()) as SessionResponse;
if (!session.isLoggedIn) return undefined;

return session.userInfo?.email;
} catch {
return undefined;
}
}

export const Route = createFileRoute("/api/feedback")({
server: {
handlers: {
Expand Down Expand Up @@ -80,12 +106,14 @@ export async function handleFeedbackPost(request: Request) {
].join("\n");
} else {
const docsFeedback = body as DocsFeedback;
const resolvedEmail = (await getAuthenticatedUserEmail(request)) ?? docsFeedback.email;

slackMessage = [
"*Docs Feedback*",
`*URL*: ${baseUrl}${docsFeedback.url}`,
`*Opinion*: ${docsFeedback.opinion === "good" ? "good" : "bad"}`,
`*Message*: \n\`\`\`${escapeTripleBackticks(docsFeedback.message || "_none_")}\`\`\``,
...(docsFeedback.email ? [`*User*: ${docsFeedback.email}`] : []),
...(resolvedEmail ? [`*User*: ${resolvedEmail}`] : []),
].join("\n");
}

Expand Down
Loading