Squashed commit of the following:

commit 263ac7d58539235c9c3762d9bee037449f7099e9
Author: Ivan Dimitrov <ivan@idimitrov.dev>
Date:   Sun Nov 12 14:11:50 2023 +0200

    touches

commit 81f56ab63bd60f27a1d2635673b3834bf31a25da
Author: Ivan Dimitrov <ivan@idimitrov.dev>
Date:   Sun Nov 12 14:00:41 2023 +0200

    writing technical details for the API

commit fc90f9b2288b4dc955ece07af43a42979398a3e7
Author: Ivan Dimitrov <ivan@idimitrov.dev>
Date:   Sun Nov 12 13:22:15 2023 +0200

    code blocks with a copy button

commit 9d08417a639cfc5673a2f00cd1fed56b6d640bb0
Author: Ivan Dimitrov <ivan@idimitrov.dev>
Date:   Sat Nov 11 21:53:55 2023 +0200

    content n sheet
This commit is contained in:
Ivan Dimitrov 2023-11-12 14:12:26 +02:00
parent fe788fb0d1
commit cda70cce35
9 changed files with 179 additions and 25 deletions

View File

@ -6,8 +6,17 @@ date: Jul 29, 2023 - Nov 5, 2023
z: 5
---
<details>
<summary>Solution</summary>
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.
</details>
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
<iframe src={`https://docs.google.com/{{"spreadsheets" or "presentation"}}/d/${docId}/preview`}></iframe>
```
```tsx
// This is used for PDFs or regular text files
<iframe src={`https://drive.google.com/file/d/${docId}/preview`}></iframe>
```

BIN
bun.lockb

Binary file not shown.

View File

@ -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",

View File

@ -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 {

View File

@ -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 (
<span className="w-full h-max p-20">
<Image className="w-full h-full" alt={alt!} height={Number(height) || imgSize} width={Number(width) || imgSize} src={`${data.slug}${src}`}></Image>
<Image className={`w-full h-full border-2 px-2 ${className || ""}`} alt={alt!} height={Number(height) || imgSize} width={Number(width) || imgSize} src={`${data.slug}${src}`}></Image>
</span>
)
},
a({ href, children, className }) {
return (
<Link className={className || ""} aria-label={href} href={href!} target="_blank">{children}</Link>
)
},
pre({ children, className }) {
return (
<CodeBlock className={className} children={children} />
)
}
}}
>
@ -72,7 +85,7 @@ export default function Content({ params }: Props) {
return (
<div className="w-full h-full p-20 overflow-x-hidden overflow-scroll">
<div className="flex flex-col gap-4 text-center">
<div className="flex flex-col gap-4 text-center border-amber-50 border-2 p-2 m-2 rounded-full">
{title()}
{goal()}
{role()}

View File

@ -4,7 +4,7 @@ import { getCases } from "../lib/content";
const Cases = () =>
<div className="p-20 w-3/4 mx-auto flex flex-col gap-4">
{getCases().filter(c => !c.data.draft).sort(c => Number(c.data.z)).reverse().map((c) => c.data).map((d) =>
<div key={d.slug} className="w-full h-max flex justify-center">
<div key={d.slug} className="w-full h-max flex justify-center rounded-lg border-2">
<Link className="btn flex flex-col w-full text-center" href={d.slug}>
<span className="text-lg px-6">{d.title}</span>
<span>{d.date}</span>

View File

@ -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 (
<div style={{ position: 'relative' }}>
<button
className="absolute top-5 right-5"
onClick={() => {
navigator.clipboard.writeText(getText(children))
setVisible("visible")
setTimeout(() => setVisible("invisible"), 1000)
}}
>
<span className={`${visible} absolute bottom-5 left-5`}><FontAwesomeIcon className="text-green-400" icon={faCheck} /></span>
<FontAwesomeIcon icon={faCopy} />
</button>
<pre className={`${className || ""}`}>{children}</pre>
</div>
)
}
export default CodeBlock;

View File

@ -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
}

View File

@ -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