wtf
This commit is contained in:
parent
9c531c047b
commit
ebcce28d5b
30 changed files with 1805 additions and 1031 deletions
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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)
|
||||
}
|
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
14
app/page.tsx
14
app/page.tsx
|
@ -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>;
|
||||
}
|
1
articles/algebra/functions.md
Normal file
1
articles/algebra/functions.md
Normal file
|
@ -0,0 +1 @@
|
|||
# What are Functions?
|
3
client/App.tsx
Normal file
3
client/App.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function App() {
|
||||
return <></>;
|
||||
}
|
12
client/index.tsx
Normal file
12
client/index.tsx
Normal 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
3
components/Container.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Container({ children }) {
|
||||
return <div style={{ margin: "0 auto", width: "980px" }}>{children}</div>;
|
||||
}
|
|
@ -2,6 +2,7 @@ import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
|
|||
import { cookies } from "next/headers";
|
||||
import LogoutButton from "../LogoutButton";
|
||||
import Link from "next/link";
|
||||
import Container from "../Container";
|
||||
|
||||
export default async function Navbar() {
|
||||
const supabase = createServerComponentClient({ cookies });
|
||||
|
@ -11,8 +12,8 @@ export default async function Navbar() {
|
|||
} = await supabase.auth.getUser();
|
||||
|
||||
return (
|
||||
<nav className="bp5-navbar bp5-dark">
|
||||
<div style={{ margin: "0 auto", width: "980px" }}>
|
||||
<nav className="bp5-navbar bp5-dark" style={{ marginBottom: "20px" }}>
|
||||
<Container>
|
||||
<div className="bp5-navbar-group bp5-align-left">
|
||||
<div className="bp5-navbar-heading">Eduproj</div>
|
||||
<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-cog"></button>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
|
7
index.html
Normal file
7
index.html
Normal 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
3
lib/mdpp.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import MarkdownIt from "markdown-it";
|
||||
|
||||
export function markdownPreprocessor(md: MarkdownIt): void {}
|
|
@ -1,6 +0,0 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
8
nodemon.json
Normal file
8
nodemon.json
Normal 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
2170
package-lock.json
generated
File diff suppressed because it is too large
Load diff
18
package.json
18
package.json
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "vite",
|
||||
"dev:server": "nodemon --watch './**/*.ts' server/index.ts",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
|
@ -12,12 +14,15 @@
|
|||
"@supabase/auth-helpers-nextjs": "latest",
|
||||
"@supabase/supabase-js": "latest",
|
||||
"autoprefixer": "10.4.14",
|
||||
"next": "latest",
|
||||
"express": "^4.18.2",
|
||||
"jsdom": "^22.1.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"postcss": "8.4.24",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"tailwindcss": "3.3.2",
|
||||
"typescript": "5.1.3"
|
||||
"typescript": "5.1.3",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^7.2.0",
|
||||
|
@ -28,10 +33,15 @@
|
|||
"@storybook/react": "^7.2.0",
|
||||
"@storybook/react-vite": "^7.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/react": "18.2.12",
|
||||
"@types/react-dom": "18.2.5",
|
||||
"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
10
server/index.ts
Normal 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);
|
16
stories/articles/Article.module.scss
Normal file
16
stories/articles/Article.module.scss
Normal 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;
|
||||
}
|
||||
}
|
36
stories/articles/Article.stories.ts
Normal file
36
stories/articles/Article.stories.ts
Normal 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>
|
||||
`,
|
||||
},
|
||||
};
|
30
stories/articles/Article.tsx
Normal file
30
stories/articles/Article.tsx
Normal 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>
|
||||
);
|
||||
}
|
59
stories/articles/ConvertNode.tsx
Normal file
59
stories/articles/ConvertNode.tsx
Normal 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");
|
||||
}
|
15
stories/articles/Exercise.tsx
Normal file
15
stories/articles/Exercise.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "es2020",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
@ -12,17 +12,16 @@
|
|||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
}
|
||||
|
|
13
vite.config.ts
Normal file
13
vite.config.ts
Normal 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/, ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
Loading…
Reference in a new issue