Skip to content
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
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ dist/
storybook-static/
next-env.d.ts

packages/icons-react/icons.tsx

# editors
.idea/
Expand All @@ -17,5 +18,4 @@ next-env.d.ts

# Secrets
.FIGMA_TOKEN
.vercel
packages/icons-react/icons.tsx
.env.local
2 changes: 2 additions & 0 deletions apps/docs/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Creata a viewer token at sanity.io/manage
SANITY_VIEWER_TOKEN=
3 changes: 3 additions & 0 deletions apps/docs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
public/resources/icons/
public/storybook
component-props.ts
docgen.ts

.env
97 changes: 97 additions & 0 deletions apps/docs/app/routes/_docs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { previewMiddleware } from '@/lib/preview-middleware';
import { sanityFetch } from '@/lib/sanity';
import { VisualEditing } from '@/lib/visual-editing';
import appCss from '@/styles/app.css?url';
import { DisablePreviewMode } from '@/ui/disable-preview-mode';
import { Footer } from '@/ui/footer';
import { MainNav } from '@/ui/main-nav';
import { GrunnmurenProvider } from '@obosbbl/grunnmuren-react';
import {
type NavigateOptions,
Outlet,
ScrollRestoration,
type ToOptions,
createFileRoute,
useRouter,
} from '@tanstack/react-router';
import { createServerFn } from '@tanstack/start';
import { defineQuery } from 'groq';

const COMPONENTS_NAVIGATION_QUERY = defineQuery(
// make sure the slug is always a string so we don't have add fallback value in code just to make TypeScript happy
`*[_type == "component"]{ _id, name, 'slug': coalesce(slug.current, '')} | order(name asc)`,
);

const checkIsPreview = createServerFn({ method: 'GET' })
.middleware([previewMiddleware])
.handler(({ context }) => {
return context.previewMode;
});

// This is the shared layout for all the Grunnmuren docs pages that are "public", ie not the Sanity studio
export const Route = createFileRoute('/_docs')({
component: RootLayout,
head: () => ({
links: [{ rel: 'stylesheet', href: appCss }],
meta: [
{
title: "Grunnmuren - OBOS' Design System",
},
],
}),
beforeLoad: async () => {
const isPreview = await checkIsPreview();
return { isPreview };
},
loader: async ({ context }) => {
return {
componentsNavItems: (
await sanityFetch({ query: COMPONENTS_NAVIGATION_QUERY })
).data,
isPreview: context.isPreview,
};
},
});

function RootLayout() {
const router = useRouter();
const { isPreview } = Route.useLoaderData();

return (
<>
<GrunnmurenProvider
locale="nb"
// This integrates RAC/Grunnmuren with TanStack router
// Giving us typesafe routes
// See https://react-spectrum.adobe.com/react-aria/routing.html#tanstack-router
navigate={(to, options) => router.navigate({ to, ...options })}
useHref={(to) => router.buildLocation({ to }).href}
>
{isPreview && (
<>
<VisualEditing />
<DisablePreviewMode />
</>
)}
<div className="grid min-h-screen lg:flex">
<div className="flex grow flex-col px-6">
<main className="grow">
<Outlet />
</main>
<Footer />
</div>
<MainNav />
</div>
</GrunnmurenProvider>
<ScrollRestoration />
</>
);
}

// See comments on GrunnmurenProvider in <RootLayout />
declare module 'react-aria-components' {
interface RouterConfig {
href: ToOptions['to'];
routerOptions: Omit<NavigateOptions, keyof ToOptions>;
}
}
54 changes: 54 additions & 0 deletions apps/docs/app/routes/_docs/komponenter/$slug.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as badgeExamples from '@/examples/badge';
import * as buttonExamples from '@/examples/button';
import { sanityFetch } from '@/lib/sanity.server';
import { Content } from '@/ui/content';
import { PropsTable } from '@/ui/props-table';
import { createFileRoute, notFound } from '@tanstack/react-router';
import * as props from 'docgen';
import { defineQuery } from 'groq';

const COMPONENT_QUERY = defineQuery(
`*[_type == "component" && slug.current == $slug][0]{ content, "name": coalesce(name, '') }`,
);

export const Route = createFileRoute('/_docs/komponenter/$slug')({
component: Page,
loader: async ({ params, context }) => {
console.log('context in component route', context);
const res = await sanityFetch({
data: {
query: COMPONENT_QUERY,
params: { slug: params.slug },
},
});

if (res.data == null) {
throw notFound();
}

const componentName = res.data.name;
const componentProps = props[componentName].props;

return { data: res.data, componentProps };
},
});

function Page() {
const { data, componentProps } = Route.useLoaderData();

// @ts-expect-error this works for now until we figure how to make the examples work better with Sanity
const { scope, examples } = {
Button: buttonExamples,
Badge: badgeExamples,
}[data.name];

return (
<>
<h1 className="heading-l mb-12 mt-9">{data.name}</h1>

<Content content={data.content ?? []} />

<PropsTable props={componentProps} />
</>
);
}
14 changes: 14 additions & 0 deletions apps/docs/app/routes/api/preview-mode/disable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createAPIFileRoute } from '@tanstack/start/api';
import { deleteCookie, sendRedirect } from 'vinxi/http';

export const APIRoute = createAPIFileRoute('/api/preview-mode/disable')({
GET: () => {
deleteCookie('__sanity_preview', {
path: '/',
secure: import.meta.env.PROD,
httpOnly: true,
sameSite: 'strict',
});
sendRedirect('/');
},
});
37 changes: 37 additions & 0 deletions apps/docs/app/routes/api/preview-mode/enable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { randomBytes } from 'node:crypto';
import { client } from '@/lib/sanity';
import { validatePreviewUrl } from '@sanity/preview-url-secret';
import { createAPIFileRoute } from '@tanstack/start/api';
import { SanityClient } from 'sanity';
import { sendRedirect, setCookie } from 'vinxi/http';

export const APIRoute = createAPIFileRoute('/api/preview-mode/enable')({
GET: async ({ request }) => {
if (!process.env.SANITY_VIEWER_TOKEN) {
throw new Response('Preview mode missing token', { status: 401 });
}

const clientWithToken = client.withConfig({
token: process.env.SANITY_VIEWER_TOKEN,
});

const { isValid, redirectTo = '/' } = await validatePreviewUrl(
clientWithToken,
request.url,
);

if (!isValid) {
throw new Response('Invalid secret', { status: 401 });
}

// we can use sameSite: 'strict' because we're running an embedded studio
// setCookie('__sanity_preview', randomBytes(16).toString('hex'), {
setCookie('__sanity_preview', 'true', {
path: '/',
secure: import.meta.env.PROD,
httpOnly: true,
sameSite: 'strict',
});
sendRedirect(redirectTo);
},
});
3 changes: 3 additions & 0 deletions apps/docs/dktp/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ properties:
containers:
- image: dktprodacr.azurecr.io/grunnmuren/docs:${IMAGE_TAG}
name: docs
env:
- name: SANITY_VIEWER_TOKEN
secretRef: ${todo}
resources:
cpu: 0.25
memory: 0.5Gi
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
"@sanity/client": "7.12.1",
"@sanity/code-input": "6.0.3",
"@sanity/image-url": "^1.2.0",
"@sanity/preview-url-secret": "2.1.15",
"@sanity/table": "2.0.0",
"@sanity/vision": "4.14.2",
"@sanity/visual-editing": "4.0.0",
"@tanstack/nitro-v2-vite-plugin": "1.133.19",
"@tanstack/react-router": "1.135.0",
"@tanstack/react-start": "1.135.0",
Expand Down
17 changes: 17 additions & 0 deletions apps/docs/sanity.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { codeInput } from '@sanity/code-input';
import { table } from '@sanity/table';
import { visionTool } from '@sanity/vision';
import { defineConfig } from 'sanity';
import { defineDocuments, presentationTool } from 'sanity/presentation';
import { structureTool } from 'sanity/structure';
import { schemaTypes } from './studio/schema-types';

Expand Down Expand Up @@ -70,6 +71,22 @@ export default defineConfig({
visionTool(),
codeInput(),
table(),
presentationTool({
previewUrl: {
previewMode: {
enable: '/api/preview-mode/enable',
disable: '/api/preview-mode/disable',
},
},
resolve: {
mainDocuments: defineDocuments([
{
route: '/komponenter/:slug',
filter: `_type == "component" && slug.current == $slug`,
},
]),
},
}),
],
schema: {
types: schemaTypes,
Expand Down
12 changes: 12 additions & 0 deletions apps/docs/src/lib/preview-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createMiddleware } from '@tanstack/start';
import { getCookie } from 'vinxi/http';

export const previewMiddleware = createMiddleware().server(({ next }) => {
const isPreview = getCookie('__sanity_preview') === 'true';
console.log('middleware', { isPreview });
return next({
context: {
previewMode: isPreview,
},
});
});
22 changes: 22 additions & 0 deletions apps/docs/src/lib/sanity.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { QueryParams } from '@sanity/client';
import { createServerFn } from '@tanstack/start';
import { previewMiddleware } from './preview-middleware';
import { sanityFetch as _sanityFetch, client } from './sanity';

export const sanityFetch = createServerFn({ method: 'GET' })
.middleware([previewMiddleware])
.validator((data: { query: string; params: QueryParams }) => data)
.handler(async ({ data, context }) => {
const { query, params } = data;

if (context.previewMode) {
const previewClient = client.withConfig({
perspective: 'previewDrafts',
token: process.env.SANITY_VIEWER_TOKEN, // Needed for accessing previewDrafts perspective
useCdn: false, // the previewDrafts perspective requires this to be `false
});
return _sanityFetch({ query, params, client: previewClient });
}

return _sanityFetch({ query, params });
});
5 changes: 4 additions & 1 deletion apps/docs/src/lib/sanity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ export const client = createClient({
dataset: 'grunnmuren',
apiVersion: '2024-09-18',
useCdn: true,
perspective: 'published',
});

export async function sanityFetch<const QueryString extends string>({
query,
params = {},
client: _client = client,
}: {
query: QueryString;
params?: QueryParams;
client?: typeof client;
}) {
// Not sure what's happening here, but I need to set filterReponse to false to get the data as an array?
const { result } = await client.fetch(query, params, {
const { result } = await _client.fetch(query, params, {
filterResponse: false,
});

Expand Down
41 changes: 41 additions & 0 deletions apps/docs/src/lib/visual-editing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { enableVisualEditing } from '@sanity/visual-editing'
import { useNavigate, useRouter } from '@tanstack/react-router';
import { useEffect } from 'react'

export function VisualEditing() {
const router = useRouter();


useEffect(() => {
const disable = enableVisualEditing({
history: {
// subscribe: (_navigate) => {
// router.history.subscribe
// },
// subscribe: (_navigate) => {
// },
// subscribe: (_navigate) => {
// setNavigate(() => _navigate)
// return () => setNavigate(undefined)
// },
update: (update) => {
console.log(update);
switch (update.type) {
case 'push':
router.history.push(update.url);
break;
case 'replace':
router.history.replace(update.url);
break;
case 'pop':
router.history.back();
break;
}
},
}
})

}, [router]);

return null;
}
Loading
Loading