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 { 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
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,
|
"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
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": {
|
"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
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