Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Nebula Chat UI #5483

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
5 changes: 4 additions & 1 deletion apps/dashboard/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,7 @@ NEXT_PUBLIC_TURNSTILE_SITE_KEY=""
TURNSTILE_SECRET_KEY=""
REDIS_URL=""

ANALYTICS_SERVICE_URL=""
ANALYTICS_SERVICE_URL=""

# Required for Nebula Chat
NEXT_PUBLIC_NEBULA_URL=""
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"color": "^4.2.3",
"compare-versions": "^6.1.0",
"date-fns": "4.1.0",
"fetch-event-stream": "^0.1.5",
"flat": "^6.0.1",
"framer-motion": "11.11.17",
"fuse.js": "7.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function ScrollShadow(props: {
scrollableClassName?: string;
disableTopShadow?: boolean;
shadowColor?: string;
shadowClassName?: string;
}) {
const scrollableEl = useRef<HTMLDivElement>(null);
const shadowTopEl = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -94,29 +95,45 @@ export function ScrollShadow(props: {
}
>
<div
className={cn(styles.scrollShadowTop, styles.scrollShadowY)}
className={cn(
styles.scrollShadowTop,
styles.scrollShadowY,
props.shadowClassName,
)}
ref={shadowTopEl}
style={{
opacity: "0",
display: props.disableTopShadow ? "none" : "block",
}}
/>
<div
className={cn(styles.scrollShadowBottom, styles.scrollShadowY)}
className={cn(
styles.scrollShadowBottom,
styles.scrollShadowY,
props.shadowClassName,
)}
ref={shadowBottomEl}
style={{
opacity: "0",
}}
/>
<div
className={cn(styles.scrollShadowLeft, styles.scrollShadowX)}
className={cn(
styles.scrollShadowLeft,
styles.scrollShadowX,
props.shadowClassName,
)}
ref={shadowLeftEl}
style={{
opacity: "0",
}}
/>
<div
className={cn(styles.scrollShadowRight, styles.scrollShadowX)}
className={cn(
styles.scrollShadowRight,
styles.scrollShadowX,
props.shadowClassName,
)}
ref={shadowRightEl}
style={{
opacity: "0",
Expand Down
7 changes: 6 additions & 1 deletion apps/dashboard/src/@/components/ui/code/code.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type CodeProps = {
scrollableClassName?: string;
keepPreviousDataOnCodeChange?: boolean;
copyButtonClassName?: string;
ignoreFormattingErrors?: boolean;
};

export const CodeClient: React.FC<CodeProps> = ({
Expand All @@ -21,10 +22,14 @@ export const CodeClient: React.FC<CodeProps> = ({
scrollableClassName,
keepPreviousDataOnCodeChange = false,
copyButtonClassName,
ignoreFormattingErrors,
}) => {
const codeQuery = useQuery({
queryKey: ["html", code],
queryFn: () => getCodeHtml(code, lang),
queryFn: () =>
getCodeHtml(code, lang, {
ignoreFormattingErrors: ignoreFormattingErrors,
}),
placeholderData: keepPreviousDataOnCodeChange
? keepPreviousData
: undefined,
Expand Down
23 changes: 16 additions & 7 deletions apps/dashboard/src/@/components/ui/code/getCodeHtml.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,28 @@ function isPrettierSupportedLang(lang: BundledLanguage) {
);
}

export async function getCodeHtml(code: string, lang: BundledLanguage) {
export async function getCodeHtml(
code: string,
lang: BundledLanguage,
options?: {
ignoreFormattingErrors?: boolean;
},
) {
const formattedCode = isPrettierSupportedLang(lang)
? await format(code, {
parser: "babel-ts",
plugins: [parserBabel, estree],
printWidth: 60,
}).catch((e) => {
console.error(e);
console.error("Failed to format code");
console.log({
code,
lang,
});
if (!options?.ignoreFormattingErrors) {
console.error(e);
console.error("Failed to format code");
console.log({
code,
lang,
});
}

return code;
})
: code;
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/@/components/ui/inline-code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export function InlineCode({
return (
<code
className={cn(
"mx-0.5 inline rounded-lg border border-border px-1.5 py-[3px] font-mono text-[0.85em] text-foreground",
"mx-0.5 inline rounded-lg border border-border bg-muted px-[0.4em] py-[0.25em] font-mono text-[0.85em] text-foreground",
className,
)}
>
Expand Down
12 changes: 12 additions & 0 deletions apps/dashboard/src/@/components/ui/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,22 @@ function useUnderline<El extends HTMLElement>() {
}

update();
let resizeObserver: ResizeObserver | undefined = undefined;

if (containerRef.current) {
resizeObserver = new ResizeObserver(() => {
setTimeout(() => {
update();
}, 100);
});
resizeObserver.observe(containerRef.current);
}

// add event listener for resize
window.addEventListener("resize", update);
return () => {
window.removeEventListener("resize", update);
resizeObserver?.disconnect();
};
}, [activeTabEl]);

Expand Down
24 changes: 24 additions & 0 deletions apps/dashboard/src/@/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,27 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
Textarea.displayName = "Textarea";

export { Textarea };

export function AutoResizeTextarea(props: TextareaProps) {
const textareaRef = React.useRef<HTMLTextAreaElement>(null);

// biome-ignore lint/correctness/useExhaustiveDependencies: value is needed in deps array
React.useEffect(() => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
}
}, [props.value]);

return (
<Textarea
ref={textareaRef}
{...props}
style={{
...props.style,
overflowY: "hidden",
}}
/>
);
}
2 changes: 2 additions & 0 deletions apps/dashboard/src/@/constants/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ export const BASE_URL = isProd
: (process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}`
: "http://localhost:3000") || "https://thirdweb-dev.com";

export const NEXT_PUBLIC_NEBULA_URL = process.env.NEXT_PUBLIC_NEBULA_URL;
2 changes: 1 addition & 1 deletion apps/dashboard/src/app/account/settings/getAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export async function getRawAccount() {
* If there's no account or account onboarding not complete, redirect to login page
* @param pagePath - the path of the current page to redirect back to after login/onboarding
*/
export async function getValidAccount(pagePath: string) {
export async function getValidAccount(pagePath?: string) {
const account = await getRawAccount();

// enforce login & onboarding
Expand Down
30 changes: 8 additions & 22 deletions apps/dashboard/src/app/login/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const wallets = [

export function LoginAndOnboardingPage(props: {
account: Account | undefined;
nextPath: string | undefined;
redirectPath: string;
}) {
return (
<div className="relative flex min-h-screen flex-col overflow-hidden bg-background">
Expand Down Expand Up @@ -81,7 +81,10 @@ export function LoginAndOnboardingPage(props: {

<main className="container z-10 flex grow flex-col items-center justify-center gap-6 py-12">
<ClientOnly ssr={<LoadingCard />}>
<PageContent nextPath={props.nextPath} account={props.account} />
<PageContent
redirectPath={props.redirectPath}
account={props.account}
/>
</ClientOnly>
</main>

Expand Down Expand Up @@ -110,7 +113,7 @@ function LoadingCard() {
}

function PageContent(props: {
nextPath: string | undefined;
redirectPath: string;
account: Account | undefined;
}) {
const [screen, setScreen] = useState<
Expand All @@ -133,11 +136,7 @@ function PageContent(props: {

function onComplete() {
setScreen({ id: "complete" });
if (props.nextPath && isValidRedirectPath(props.nextPath)) {
router.replace(props.nextPath);
} else {
router.replace("/team");
}
router.replace(props.redirectPath);
}

if (connectionStatus === "connecting") {
Expand All @@ -154,7 +153,7 @@ function PageContent(props: {
<LazyOnboardingUI
account={screen.account}
onComplete={onComplete}
redirectPath={props.nextPath || "/team"}
redirectPath={props.redirectPath}
/>
</Suspense>
);
Expand Down Expand Up @@ -220,19 +219,6 @@ function CustomConnectEmbed(props: {
);
}

function isValidRedirectPath(encodedPath: string): boolean {
try {
// Decode the URI component
const decodedPath = decodeURIComponent(encodedPath);
// ensure the path always starts with a _single_ slash
// double slash could be interpreted as `//example.com` which is not allowed
return decodedPath.startsWith("/") && !decodedPath.startsWith("//");
} catch {
// If decoding fails, return false
return false;
}
}

type AuroraProps = {
size: { width: string; height: string };
pos: { top: string; left: string };
Expand Down
1 change: 0 additions & 1 deletion apps/dashboard/src/app/login/auth-actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"use server";
import "server-only";

import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
Expand Down
12 changes: 12 additions & 0 deletions apps/dashboard/src/app/login/isValidEncodedRedirectPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function isValidEncodedRedirectPath(encodedPath: string): boolean {
try {
// Decode the URI component
const decodedPath = decodeURIComponent(encodedPath);
// ensure the path always starts with a _single_ slash
// double slash could be interpreted as `//example.com` which is not allowed
return decodedPath.startsWith("/") && !decodedPath.startsWith("//");
} catch {
// If decoding fails, return false
return false;
}
}
6 changes: 5 additions & 1 deletion apps/dashboard/src/app/login/loginRedirect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { redirect } from "next/navigation";

export function loginRedirect(path: string): never {
export function loginRedirect(path?: string): never {
if (!path) {
redirect("/login");
}

redirect(`/login?next=${encodeURIComponent(path)}`);
}
14 changes: 8 additions & 6 deletions apps/dashboard/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { redirect } from "next/navigation";
import { getRawAccount } from "../account/settings/getAccount";
import { LoginAndOnboardingPage } from "./LoginPage";
import { isValidEncodedRedirectPath } from "./isValidEncodedRedirectPath";
import { isOnboardingComplete } from "./onboarding/isOnboardingRequired";

export default async function Page(props: {
Expand All @@ -11,13 +12,14 @@ export default async function Page(props: {
const nextPath = (await props.searchParams).next;
const account = await getRawAccount();

const redirectPath =
nextPath && isValidEncodedRedirectPath(nextPath) ? nextPath : "/team";

if (account && isOnboardingComplete(account)) {
if (nextPath) {
redirect(nextPath);
} else {
redirect("/team");
}
redirect(redirectPath);
}

return <LoginAndOnboardingPage account={account} nextPath={nextPath} />;
return (
<LoginAndOnboardingPage account={account} redirectPath={redirectPath} />
);
}
Loading