diff --git a/_content/cases/stepsy.wiki.md b/_content/cases/stepsy.wiki.md index 0349747..4312134 100644 --- a/_content/cases/stepsy.wiki.md +++ b/_content/cases/stepsy.wiki.md @@ -6,8 +6,17 @@ date: Jul 29, 2023 - Nov 5, 2023 z: 5 --- -
-Solution +This project aims to be a Google Drive frontend. It uses the Google APIs to fetch document data and display that data in a wiki-style web page. + + +![thumbnail](/thumbnail.png) + + +It supports Google Docs, Google Sheets, Google Slides, PDFs and regular files. + +--- + +### Technical overview I chose NextJS as the backbone for this project as it offers the greatest amount of flexibility while still being very powerful both on the client as well as on the server with an active community and thriving ecosystem. @@ -31,12 +40,100 @@ The UI/UX uses TailwindCSS and DaisyUI to make everything a fast, modern, optimi React was used with TypeScript to provide a nice modern client-side experience between transitions and interactions. This setup supports maximum optimization as you can see in the screenshots below allowing the app to reach a lighthouse score of 100 on all but one (it has 99) pages. Both mobile and desktop is supported. -
- -This project aims to be a Google Drive frontend. It uses the Google APIs to fetch document data and display that data in a wiki-style web page. -![thumbnail](/thumbnail.png) +--- -It supports Google Docs, Google Sheets, Google Slides, PDFs and regular files. +### Google API details + +Configure NextAuth for Google: + +```ts +export default NextAuth({ +... + providers: [ + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + authorization: { + params: { + access_type: "offline", + prompt: "consent", + scope: "openid profile email https://www.googleapis.com/auth/drive", + }, + }, + }), + ], +... +``` + +Create an auth client for logged in users + +```ts +let authClient = new google.auth.OAuth2( + process.env.GOOGLE_CLIENT_ID, + process.env.GOOGLE_CLIENT_SECRET, +); +authClient.setCredentials({ + access_token: accessToken, // this comes from the logged in user info + refresh_token: refreshToken, // same for this +}); +``` + +or one for anonymous users using a Google service account + +```ts +authClient = new google.auth.JWT({ + email: serviceAccount.client_email, + key: serviceAccount.private_key, + scopes: ["https://www.googleapis.com/auth/drive"], +}); +``` + +Create the drive client + +```ts +const drive = google.drive({ + version: "v3", + auth: authClient, +}); +``` + +You can now use this client to query the API + +```ts +const file = (await drive.files.get({fileId})).data; +``` + +```ts +const folderContents = (await drive.files.list({ q: `'${folderId}' in parents` })) + .data.files; +``` + +```ts +const googleDocHtml = (await drive.files.export({ + fileId: googleDocId, + mimeType: "text/html", +})).data; +``` + +```ts +const shortcutTarget = await drive.files.get({ + fileId, + fields: "shortcutDetails/targetId", +}); +const targetId = shortcutTarget.data.shortcutDetails?.targetId +``` + +Google doesn't export everything to HTML. They provide document renderers as iFrames. + +```tsx + +``` + +```tsx +// This is used for PDFs or regular text files + +``` + diff --git a/bun.lockb b/bun.lockb index d9a9cae..cfff84b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 97800ad..c80645c 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "latest", "gray-matter": "^4.0.3", + "highlight.js": "^11.9.0", "next": "latest", "react": "latest", "react-dom": "latest", @@ -23,8 +24,7 @@ "rehype-highlight": "^7.0.0", "rehype-raw": "^7.0.0", "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "tailwind-highlightjs": "^2.0.1" + "remark-gfm": "^4.0.0" }, "devDependencies": { "typescript": "latest", diff --git a/src/app/c/[...slug]/content.module.css b/src/app/c/[...slug]/content.module.css index 2b55e18..54cbbbc 100644 --- a/src/app/c/[...slug]/content.module.css +++ b/src/app/c/[...slug]/content.module.css @@ -1,3 +1,7 @@ +.md * { + @apply py-2 +} + .md ol { @apply list-decimal list-inside } @@ -5,6 +9,10 @@ @apply list-disc list-inside } +.md hr { + @apply m-4 +} + .md blockquote { @apply border-l-neutral-500 border-l-4 p-2 } @@ -17,8 +25,8 @@ @apply p-20 } -.md details * { - @apply p-2 +.md code { + @apply rounded-lg } .md h1 { diff --git a/src/app/c/[...slug]/page.tsx b/src/app/c/[...slug]/page.tsx index 11f0ad6..6843292 100644 --- a/src/app/c/[...slug]/page.tsx +++ b/src/app/c/[...slug]/page.tsx @@ -1,12 +1,15 @@ +import "highlight.js/styles/github-dark.css" +import styles from "./content.module.css" import { getAllPaths, getContent } from "$lib/content"; import Markdown from "react-markdown"; import remarkGfm from "remark-gfm"; import remarkFrontmatter from "remark-frontmatter"; -import styles from "./content.module.css" import Image from "next/image"; import rehypeRaw from "rehype-raw"; import rehypeHighlight from "rehype-highlight"; import { notFound } from "next/navigation"; +import Link from "next/link"; +import CodeBlock from "$components/code-block"; type Params = { slug: string[] @@ -58,12 +61,22 @@ export default function Content({ params }: Props) { remarkPlugins={[remarkGfm, remarkFrontmatter]} rehypePlugins={[rehypeRaw, rehypeHighlight]} components={{ - img({ height, width, src, alt }) { + img({ height, width, src, alt, className }) { return ( - {alt!} + {alt!} ) + }, + a({ href, children, className }) { + return ( + {children} + ) + }, + pre({ children, className }) { + return ( + + ) } }} > @@ -72,7 +85,7 @@ export default function Content({ params }: Props) { return (
-
+
{title()} {goal()} {role()} diff --git a/src/app/components/cases.tsx b/src/app/components/cases.tsx index 42f9abf..ad53d50 100644 --- a/src/app/components/cases.tsx +++ b/src/app/components/cases.tsx @@ -4,7 +4,7 @@ import { getCases } from "../lib/content"; const Cases = () =>
{getCases().filter(c => !c.data.draft).sort(c => Number(c.data.z)).reverse().map((c) => c.data).map((d) => -
+
{d.title} {d.date} diff --git a/src/app/components/code-block.tsx b/src/app/components/code-block.tsx new file mode 100644 index 0000000..5fcba8a --- /dev/null +++ b/src/app/components/code-block.tsx @@ -0,0 +1,40 @@ +"use client" +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { ReactNode, useState } from "react" + +type Props = { + className?: string + children?: ReactNode +} + +const getText = (node: any) => { + const props = node.props + if (!props) { + return node + } + const c = props.children + return typeof c === "string" ? c : c.map(getText).join("") +} + +const CodeBlock = ({ className, children }: Props) => { + const [visible, setVisible] = useState("invisible") + return ( +
+ +
{children}
+
+ ) +} + +export default CodeBlock; diff --git a/src/app/globals.css b/src/app/globals.css index da968af..16d3fbd 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -10,6 +10,10 @@ main { @apply flex flex-col w-full h-full } +main * { + @apply border-amber-50 +} + svg { @apply w-14 h-14 text-amber-100 hover:text-cyan-500 } diff --git a/tailwind.config.ts b/tailwind.config.ts index 0d293a4..b27a408 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -6,14 +6,6 @@ const config: Config = { './src/components/**/*.{js,ts,jsx,tsx,mdx}', './src/app/**/*.{js,ts,jsx,tsx,mdx}', ], - safelist: [{ - pattern: /hljs+/, - }], - theme: { - hljs: { - theme: 'night-owl', - }, - }, - plugins: [require('tailwind-highlightjs')], + plugins: [], } export default config