This commit is contained in:
Michael Zhang 2023-08-01 22:20:15 -04:00
parent 9c531c047b
commit ebcce28d5b
30 changed files with 1805 additions and 1031 deletions

View file

@ -1,29 +0,0 @@
'use client'
// TODO: Duplicate or move this file outside the `_examples` folder to make it a route
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { useEffect, useState } from 'react'
export default function ClientComponent() {
const [todos, setTodos] = useState<any[]>([])
// Create a Supabase client configured to use cookies
const supabase = createClientComponentClient()
useEffect(() => {
const getTodos = async () => {
// This assumes you have a `todos` table in Supabase. Check out
// the `Create Table and seed with data` section of the README 👇
// https://github.com/vercel/next.js/blob/canary/examples/with-supabase/README.md
const { data } = await supabase.from('todos').select()
if (data) {
setTodos(data)
}
}
getTodos()
}, [supabase, setTodos])
return <pre>{JSON.stringify(todos, null, 2)}</pre>
}

View file

@ -1,83 +0,0 @@
// TODO: Duplicate or move this file outside the `_examples` folder to make it a route
import {
createServerActionClient,
createServerComponentClient,
} from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import Image from 'next/image'
import { redirect } from 'next/navigation'
export const dynamic = 'force-dynamic'
export default async function ProtectedRoute() {
const supabase = createServerComponentClient({ cookies })
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
// This route can only be accessed by authenticated users.
// Unauthenticated users will be redirected to the `/login` route.
redirect('/login')
}
const signOut = async () => {
'use server'
const supabase = createServerActionClient({ cookies })
await supabase.auth.signOut()
redirect('/login')
}
return (
<div className="flex-1 flex flex-col max-w-3xl mt-24">
<h1 className="text-2xl mb-2 flex justify-between">
<span className="sr-only">Supabase and Next.js Starter Template</span>
</h1>
<div className="flex border-b py-3 text-sm text-neutral-100">
<div className="flex items-center justify-between w-full">
<code className="bg-neutral-700 px-3 py-1 rounded-lg text-sm">
Protected page
</code>
<span className="flex gap-4">
Hey, {user.email}! <span className="border-r"></span>{' '}
<form action={signOut}>
<button className="text-neutral-100">Logout</button>
</form>
</span>
</div>
</div>
<div className="flex gap-8 justify-center mt-12">
<Image
src="/supabase.svg"
alt="Supabase Logo"
width={225}
height={45}
priority
/>
<div className="border-l rotate-45 h-10"></div>
<Image
src="/next.svg"
alt="Vercel Logo"
width={150}
height={36}
priority
/>
</div>
<p className="text-3xl mx-auto max-w-2xl text-center mt-8 text-white">
The fastest way to get started building apps with{' '}
<strong>Supabase</strong> and <strong>Next.js</strong>
</p>
<div className="flex justify-center mt-12">
<span className="bg-neutral-100 py-3 px-6 rounded-lg font-mono text-sm text-neutral-900">
Get started by editing <strong>app/page.tsx</strong>
</span>
</div>
</div>
)
}

View file

@ -1,19 +0,0 @@
// TODO: Duplicate or move this file outside the `_examples` folder to make it a route
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export const dynamic = 'force-dynamic'
export async function GET() {
// Create a Supabase client configured to use cookies
const supabase = createRouteHandlerClient({ cookies })
// This assumes you have a `todos` table in Supabase. Check out
// the `Create Table and seed with data` section of the README 👇
// https://github.com/vercel/next.js/blob/canary/examples/with-supabase/README.md
const { data: todos } = await supabase.from('todos').select()
return NextResponse.json(todos)
}

View file

@ -1,31 +0,0 @@
// TODO: Duplicate or move this file outside the `_examples` folder to make it a route
import { createServerActionClient } from '@supabase/auth-helpers-nextjs'
import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'
export const dynamic = 'force-dynamic'
export default async function ServerAction() {
const addTodo = async (formData: FormData) => {
'use server'
const title = formData.get('title')
if (title) {
// Create a Supabase client configured to use cookies
const supabase = createServerActionClient({ cookies })
// This assumes you have a `todos` table in Supabase. Check out
// the `Create Table and seed with data` section of the README 👇
// https://github.com/vercel/next.js/blob/canary/examples/with-supabase/README.md
await supabase.from('todos').insert({ title })
revalidatePath('/server-action-example')
}
}
return (
<form action={addTodo}>
<input name="title" />
</form>
)
}

View file

@ -1,18 +0,0 @@
// TODO: Duplicate or move this file outside the `_examples` folder to make it a route
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
export const dynamic = 'force-dynamic'
export default async function ServerComponent() {
// Create a Supabase client configured to use cookies
const supabase = createServerComponentClient({ cookies })
// This assumes you have a `todos` table in Supabase. Check out
// the `Create Table and seed with data` section of the README 👇
// https://github.com/vercel/next.js/blob/canary/examples/with-supabase/README.md
const { data: todos } = await supabase.from('todos').select()
return <pre>{JSON.stringify(todos, null, 2)}</pre>
}

View file

@ -1,21 +0,0 @@
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export const dynamic = 'force-dynamic'
export async function GET(request: Request) {
// The `/auth/callback` route is required for the server-side auth flow implemented
// by the Auth Helpers package. It exchanges an auth code for the user's session.
// https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
if (code) {
const supabase = createRouteHandlerClient({ cookies })
await supabase.auth.exchangeCodeForSession(code)
}
// URL to redirect to after sign in process completes
return NextResponse.redirect(requestUrl.origin)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View file

@ -1,42 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 200 20% 98%;
--btn-background: 200 10% 91%;
--btn-background-hover: 200 10% 89%;
--foreground: 200 50% 3%;
}
@media (prefers-color-scheme: dark) {
:root {
--background: 200 50% 3%;
--btn-background: 200 10% 9%;
--btn-background-hover: 200 10% 12%;
--foreground: 200 20% 96%;
}
}
}
@layer base {
* {
@apply border-foreground/20;
}
}
.animate-in {
animation: animateIn 0.3s ease 0.15s both;
}
@keyframes animateIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View file

@ -1,27 +0,0 @@
// import './globals.css'
import "normalize.css";
import "@blueprintjs/core/lib/css/blueprint.css";
import "@blueprintjs/icons/lib/css/blueprint-icons.css";
import Navbar from "@/components/layout/Navbar";
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<main className="min-h-screen bg-background flex flex-col items-center">
<Navbar />
{children}
</main>
</body>
</html>
);
}

View file

@ -1,126 +0,0 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import Link from 'next/link'
export default function Login() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [view, setView] = useState('sign-in')
const router = useRouter()
const supabase = createClientComponentClient()
const handleSignUp = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${location.origin}/auth/callback`,
},
})
setView('check-email')
}
const handleSignIn = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
await supabase.auth.signInWithPassword({
email,
password,
})
router.push('/')
router.refresh()
}
return (
<div className="flex-1 flex flex-col w-full px-8 sm:max-w-md justify-center gap-2">
<Link
href="/"
className="absolute left-8 top-8 py-2 px-4 rounded-md no-underline text-foreground bg-btn-background hover:bg-btn-background-hover flex items-center group text-sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1"
>
<polyline points="15 18 9 12 15 6" />
</svg>{' '}
Back
</Link>
{view === 'check-email' ? (
<p className="text-center text-foreground">
Check <span className="font-bold">{email}</span> to continue signing
up
</p>
) : (
<form
className="flex-1 flex flex-col w-full justify-center gap-2 text-foreground"
onSubmit={view === 'sign-in' ? handleSignIn : handleSignUp}
>
<label className="text-md" htmlFor="email">
Email
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="email"
onChange={(e) => setEmail(e.target.value)}
value={email}
placeholder="you@example.com"
/>
<label className="text-md" htmlFor="password">
Password
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
type="password"
name="password"
onChange={(e) => setPassword(e.target.value)}
value={password}
placeholder="••••••••"
/>
{view === 'sign-in' && (
<>
<button className="bg-green-700 rounded px-4 py-2 text-white mb-6">
Sign In
</button>
<p className="text-sm text-center">
Don't have an account?
<button
className="ml-1 underline"
onClick={() => setView('sign-up')}
>
Sign Up Now
</button>
</p>
</>
)}
{view === 'sign-up' && (
<>
<button className="bg-green-700 rounded px-4 py-2 text-white mb-6">
Sign Up
</button>
<p className="text-sm text-center">
Already have an account?
<button
className="ml-1 underline"
onClick={() => setView('sign-in')}
>
Sign In Now
</button>
</p>
</>
)}
</form>
)}
</div>
)
}

View file

@ -1,14 +0,0 @@
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
export const dynamic = "force-dynamic";
export default async function Index() {
const supabase = createServerComponentClient({ cookies });
const {
data: { user },
} = await supabase.auth.getUser();
return <div className="w-full flex flex-col items-center"></div>;
}

View file

@ -0,0 +1 @@
# What are Functions?

3
client/App.tsx Normal file
View file

@ -0,0 +1,3 @@
export default function App() {
return <></>;
}

12
client/index.tsx Normal file
View file

@ -0,0 +1,12 @@
import { createRoot } from "react-dom/client";
import App from "./App";
import { StrictMode } from "react";
const domNode = document.getElementById("app")!;
const root = createRoot(domNode);
root.render(
<StrictMode>
<App />
</StrictMode>
);

3
components/Container.tsx Normal file
View file

@ -0,0 +1,3 @@
export default function Container({ children }) {
return <div style={{ margin: "0 auto", width: "980px" }}>{children}</div>;
}

View file

@ -2,6 +2,7 @@ import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import LogoutButton from "../LogoutButton"; import LogoutButton from "../LogoutButton";
import Link from "next/link"; import Link from "next/link";
import Container from "../Container";
export default async function Navbar() { export default async function Navbar() {
const supabase = createServerComponentClient({ cookies }); const supabase = createServerComponentClient({ cookies });
@ -11,8 +12,8 @@ export default async function Navbar() {
} = await supabase.auth.getUser(); } = await supabase.auth.getUser();
return ( return (
<nav className="bp5-navbar bp5-dark"> <nav className="bp5-navbar bp5-dark" style={{ marginBottom: "20px" }}>
<div style={{ margin: "0 auto", width: "980px" }}> <Container>
<div className="bp5-navbar-group bp5-align-left"> <div className="bp5-navbar-group bp5-align-left">
<div className="bp5-navbar-heading">Eduproj</div> <div className="bp5-navbar-heading">Eduproj</div>
<span className="bp5-navbar-divider"></span> <span className="bp5-navbar-divider"></span>
@ -43,7 +44,7 @@ export default async function Navbar() {
<button className="bp5-button bp5-minimal bp5-icon-notifications"></button> <button className="bp5-button bp5-minimal bp5-icon-notifications"></button>
<button className="bp5-button bp5-minimal bp5-icon-cog"></button> <button className="bp5-button bp5-minimal bp5-icon-cog"></button>
</div> </div>
</div> </Container>
</nav> </nav>
); );
} }

7
index.html Normal file
View file

@ -0,0 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<body>
<div id="app"></div>
<script type="module" src="client/index.tsx"></script>
</body>
</html>

3
lib/mdpp.ts Normal file
View file

@ -0,0 +1,3 @@
import MarkdownIt from "markdown-it";
export function markdownPreprocessor(md: MarkdownIt): void {}

View file

@ -1,6 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {},
};
module.exports = nextConfig;

8
nodemon.json Normal file
View file

@ -0,0 +1,8 @@
{
"watch": ["src"],
"ext": "ts",
"execMap": {
"ts": "node --loader ts-node/esm -r tsconfig-paths/register"
},
"ignore": ["src/**/*.spec.ts"]
}

2170
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,9 @@
{ {
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"dev": "next dev", "dev": "vite",
"dev:server": "nodemon --watch './**/*.ts' server/index.ts",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
@ -12,12 +14,15 @@
"@supabase/auth-helpers-nextjs": "latest", "@supabase/auth-helpers-nextjs": "latest",
"@supabase/supabase-js": "latest", "@supabase/supabase-js": "latest",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"next": "latest", "express": "^4.18.2",
"jsdom": "^22.1.0",
"markdown-it": "^13.0.1",
"postcss": "8.4.24", "postcss": "8.4.24",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
"typescript": "5.1.3" "typescript": "5.1.3",
"yaml": "^2.3.1"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.2.0", "@storybook/addon-essentials": "^7.2.0",
@ -28,10 +33,15 @@
"@storybook/react": "^7.2.0", "@storybook/react": "^7.2.0",
"@storybook/react-vite": "^7.2.0", "@storybook/react-vite": "^7.2.0",
"@storybook/testing-library": "^0.2.0", "@storybook/testing-library": "^0.2.0",
"@types/jsdom": "^21.1.1",
"@types/jsdom-global": "^3.0.4",
"@types/koa": "^2.13.8",
"@types/node": "20.3.1", "@types/node": "20.3.1",
"@types/react": "18.2.12", "@types/react": "18.2.12",
"@types/react-dom": "18.2.5", "@types/react-dom": "18.2.5",
"encoding": "^0.1.13", "encoding": "^0.1.13",
"storybook": "^7.2.0" "nodemon": "^3.0.1",
"storybook": "^7.2.0",
"vite": "^4.4.8"
} }
} }

10
server/index.ts Normal file
View file

@ -0,0 +1,10 @@
import koa from "koa";
const app = new koa();
app.use(async (ctx) => {
ctx.body = "Hello World";
});
console.log("Listening...");
app.listen(6110);

View file

@ -0,0 +1,16 @@
.article {
font-family: sans-serif;
border-radius: 4px;
padding: 20px;
border: 1px solid #e0e0e0;
box-shadow: 0px 1px 4px #e8e8e8;
:first-child {
margin-top: 0;
}
:last-child {
margin-bottom: 0;
}
}

View file

@ -0,0 +1,36 @@
import { Meta, StoryObj } from "@storybook/react";
import { Article } from "./Article";
const meta: Meta<typeof Article> = {
title: "Article/Article",
component: Article,
};
export default meta;
type Story = StoryObj<typeof Article>;
export const Default: Story = {
args: {
source: "This is **markdown** content.",
},
};
export const WithExercise: Story = {
args: {
source: `
## What is addition?
1 + 1 = 2 lmao
ok now do an exercise:
<script type="eduproj/exercise">
title: Simple Addition
contributes_to:
- io.mzhang.eduproj/addition
</script>
`,
},
};

View file

@ -0,0 +1,30 @@
"use client";
import mkMarkdownIt from "markdown-it";
import styles from "./Article.module.scss";
import { markdownPreprocessor } from "../../lib/mdpp";
import ConvertNode from "./ConvertNode";
const markdownIt = mkMarkdownIt({
html: true,
linkify: true,
}).use(markdownPreprocessor);
export interface ArticleProps {
source: string;
}
export function Article({ source }: ArticleProps) {
const compiled = markdownIt.render(source);
// const parser = new window.DOMParser();
// const result = parser.parseFromString(compiled, "text/html");
return (
<div className={styles.article}>
{compiled}
{/* {[...result.body.childNodes].map((x) => (
<ConvertNode node={x} />
))} */}
</div>
);
}

View file

@ -0,0 +1,59 @@
import { createElement } from "react";
import Exercise from "./Exercise";
import { parse } from "yaml";
interface ConvertNodeProps {
node: ChildNode;
}
/**
*/
export default function ConvertNode({ node }: ConvertNodeProps): JSX.Element {
switch (node.nodeType) {
// ELEMENT_NODE
case 1: {
const element = node as Element;
const tagName = element.tagName.toLowerCase();
switch (tagName) {
case "script": {
const scriptType = element.attributes.getNamedItem("type")?.value;
console.log("script type", scriptType);
switch (scriptType) {
case "eduproj/exercise": {
console.log(element.textContent);
const data = parse(element.textContent);
console.log(data);
return <Exercise data={data} />;
}
default:
break;
}
break;
}
default: {
const props = {};
const children = [...node.childNodes].map((child) => (
<ConvertNode node={child} />
));
return createElement(tagName, props, children);
}
}
break;
}
// TEXT_NODE
case 3:
return <>{node.textContent}</>;
default:
console.log("unknown node type", node.nodeType);
return <></>;
}
throw new Error("Something not handled");
}

View file

@ -0,0 +1,15 @@
export interface ExerciseData {
title: string;
}
export interface ExerciseProps {
data: ExerciseData;
}
export default function Exercise({ data }: ExerciseProps) {
return (
<div>
<h3>Exercise</h3>
</div>
);
}

View file

@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
@ -12,17 +12,16 @@
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next"
} }
], ],
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["./*"] "@/*": ["./*"]
} }
}, }
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
} }

13
vite.config.ts Normal file
View file

@ -0,0 +1,13 @@
import { defineConfig } from "vite";
export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:6110",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
});