├── .env ├── .gitignore ├── README.md ├── app ├── api │ ├── auth │ │ └── [...nextauth] │ │ │ └── route.ts │ └── comments │ │ └── [[...comment]] │ │ └── route.ts ├── blog │ ├── [id] │ │ ├── comment.tsx │ │ ├── page.client.tsx │ │ └── page.tsx │ ├── card.tsx │ └── page.tsx ├── favicon.ico ├── globals.css ├── layout.client.tsx ├── layout.tsx ├── page.tsx ├── playground │ ├── demo │ │ ├── api.ts │ │ └── page.tsx │ ├── layout.tsx │ ├── math │ │ └── page.tsx │ ├── page.tsx │ ├── physic │ │ └── page.tsx │ └── webgl │ │ └── page.tsx ├── projects │ └── page.tsx ├── rss.xml │ └── route.ts ├── sitemap.ts └── source.ts ├── auth.ts ├── components ├── canvas │ ├── math.tsx │ ├── trigonometry.tsx │ └── webgl.tsx ├── hover-card.tsx ├── music-banner.tsx ├── nav.tsx ├── spotify.tsx ├── svg-components.tsx ├── ui │ ├── 3d-shell.tsx │ └── scroll-area.tsx └── youtube.tsx ├── content ├── about-html.mdx ├── canvas.mdx ├── fumadocs.mdx ├── good-docs.mdx ├── good-library.mdx ├── hover-card.mdx ├── music.mdx ├── my-story.mdx ├── saas-ideas.mdx ├── shit.mdx ├── sql-sorting.mdx ├── svg-art.mdx ├── svg.mdx └── ui.mdx ├── dev └── compose.yaml ├── lib ├── cn.ts ├── metadata.ts └── prisma.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prisma ├── migrations │ ├── 0_init │ │ └── migration.sql │ ├── 20240724060423_2 │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── banner.png ├── blog │ ├── clerk-toc-highlight.png │ ├── fumadocs-toc.png │ ├── toc-2.png │ ├── toc-3.png │ └── toc.png ├── eve.jpg ├── iori-kanzaki.jpg ├── markup-studio.png ├── me.jpg ├── ui-before.png ├── ui-compare.png └── yorushika.jpg └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | # It is made public so that you can play with it locally 2 | 3 | # My Anime List 4 | MAL_CLIENT_ID=d854641d6c3fff98b0192df50e7867b2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | /.content/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | others 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env*.local 32 | .env*.production 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Fuma Nama 2 | 3 | Here's the home of my personal website built using Next.js App Router and MDX. 4 | 5 | Welcome to give it a star if you appreciate my work! 6 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/auth"; 2 | export const { GET, POST } = handlers; 3 | -------------------------------------------------------------------------------- /app/api/comments/[[...comment]]/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@/auth"; 2 | import { prisma } from "@/lib/prisma"; 3 | import { NextComment } from "@fuma-comment/server/next"; 4 | import { createPrismaAdapter } from "@fuma-comment/server/adapters/prisma"; 5 | 6 | const storage = createPrismaAdapter({ 7 | db: prisma, 8 | auth: 'next-auth' 9 | }); 10 | 11 | export const { GET, DELETE, PATCH, POST } = NextComment({ 12 | storage, 13 | auth: { 14 | async getSession() { 15 | const session = await auth(); 16 | if (!session?.user?.email) return null; 17 | 18 | return { 19 | id: session.user.email, 20 | }; 21 | }, 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /app/blog/[id]/comment.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { signIn } from "next-auth/react"; 3 | import { Comments } from "@fuma-comment/react"; 4 | 5 | export function CommentsWithAuth({ page }: { page: string }) { 6 | return ( 7 | void signIn("github"), 11 | }} 12 | className="mt-4 -mx-6 sm:-mx-3 overflow-hidden" 13 | page={page} 14 | /> 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/blog/[id]/page.client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { type HTMLAttributes, useEffect, useState } from "react"; 3 | 4 | export function Date({ 5 | value, 6 | ...props 7 | }: { value: Date } & HTMLAttributes) { 8 | const [date, setDate] = useState(""); 9 | 10 | useEffect(() => { 11 | setDate(value.toLocaleDateString(undefined, { dateStyle: "full" })); 12 | }, [value]); 13 | 14 | return {date}; 15 | } 16 | -------------------------------------------------------------------------------- /app/blog/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { documents } from "@/app/source"; 2 | import { cn } from "@/lib/cn"; 3 | import Link from "next/link"; 4 | import { notFound } from "next/navigation"; 5 | import { Date } from "./page.client"; 6 | import { createMetadata } from "@/lib/metadata"; 7 | import { HTMLAttributes, type AnchorHTMLAttributes } from "react"; 8 | import dynamic from "next/dynamic"; 9 | 10 | const CommentsWithAuth = dynamic( 11 | () => import("./comment").then((res) => res.CommentsWithAuth), 12 | ); 13 | 14 | function MDXLink({ href, ...props }: AnchorHTMLAttributes) { 15 | if (!href) return ; 16 | 17 | const isExternal = href.startsWith("https://") || href.startsWith("http://"); 18 | 19 | if (isExternal) { 20 | return ( 21 | 22 | ); 23 | } 24 | 25 | return ; 26 | } 27 | 28 | const headingTypes = ["h1", "h2", "h3", "h4", "h5", "h6"] as const; 29 | 30 | function Heading({ 31 | as: As, 32 | ...props 33 | }: { as: (typeof headingTypes)[number] } & HTMLAttributes) { 34 | if (props.id) 35 | return ( 36 | 37 | 38 | 39 | # 40 | 41 | {props.children} 42 | 43 | 44 | ); 45 | 46 | return {props.children}; 47 | } 48 | 49 | export default async function Page({ params }: { params: Promise<{ id: string }> }) { 50 | const { id } = await params; 51 | const document = documents.find((d) => d.id === id); 52 | if (!document) notFound(); 53 | 54 | return ( 55 | <> 56 |
57 | , 61 | ...Object.fromEntries( 62 | headingTypes.map((type) => [ 63 | type, 64 | (props: HTMLAttributes) => ( 65 | 66 | ), 67 | ]) 68 | ), 69 | pre: ({ className, style: _style, ...props }) => ( 70 |
 77 |                 {props.children}
 78 |               
79 | ), 80 | }} 81 | /> 82 |
83 |

84 | Last Updated: 85 | 86 |

87 |
88 |
89 |

Fuma Nama

90 |

An open-sourcerer.

91 |
92 | 96 | Back to blog 97 | 98 |
99 | 100 | 101 | ); 102 | } 103 | 104 | export function generateStaticParams() { 105 | return documents.map((d) => ({ 106 | id: d.id, 107 | })); 108 | } 109 | 110 | export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) { 111 | const { id } = await params; 112 | const document = documents.find((d) => d.id === id); 113 | if (!document) notFound(); 114 | 115 | return createMetadata({ 116 | title: document.info.title, 117 | description: document.info.description, 118 | openGraph: { 119 | type: "article", 120 | authors: "Fuma Nama", 121 | modifiedTime: document.info.date.toISOString(), 122 | }, 123 | }); 124 | } 125 | -------------------------------------------------------------------------------- /app/blog/card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import { documents } from "@/app/source"; 4 | import { useLayoutEffect, useState } from "react"; 5 | 6 | export function Card({ 7 | id, 8 | info, 9 | }: { 10 | id: string; 11 | info: (typeof documents)[number]["info"]; 12 | }) { 13 | const [date, setDate] = useState(""); 14 | 15 | useLayoutEffect(() => { 16 | setDate(info.date.toLocaleDateString(undefined, { dateStyle: "medium" })); 17 | }, [info.date]); 18 | 19 | return ( 20 | 24 |
25 |
26 |

{info.title}

27 | {date} 28 |
29 |

{info.description}

30 |
31 |
32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/blog/page.tsx: -------------------------------------------------------------------------------- 1 | import { createMetadata } from "@/lib/metadata"; 2 | import { documents } from "@/app/source"; 3 | import { Card } from "./card"; 4 | 5 | export const metadata = createMetadata({ 6 | title: "Blog", 7 | description: "My precious thoughts and inspirations.", 8 | }); 9 | 10 | export default function Page() { 11 | return ( 12 |
13 |

Blog

14 |

15 | My precious thoughts and inspirations. 16 |

17 |
18 | {documents.map((d) => ( 19 | 20 | ))} 21 |
22 |
23 |

24 | "I know that I know nothing" - Socrates 25 |

26 | 32 | RSS 33 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "@fuma-comment/react/preset.css"; 3 | 4 | /* path of the package relative to the CSS file */ 5 | @source '../node_modules/@fuma-comment/react/dist/**/*.js'; 6 | @plugin "@tailwindcss/typography"; 7 | 8 | @theme { 9 | --animate-shell-show: shell-show 0.2s ease-out; 10 | 11 | @keyframes shell-show { 12 | from { 13 | transform: scale(0.7); 14 | opacity: 0%; 15 | } 16 | to { 17 | transform: scale(1); 18 | opacity: 100%; 19 | } 20 | } 21 | 22 | @keyframes shell-hide { 23 | from { 24 | transform: scale(1); 25 | opacity: "100%", 26 | } 27 | to { 28 | transform: scale(0); 29 | opacity: 0%; 30 | } 31 | } 32 | } 33 | 34 | .hover-card { 35 | --opacity: 0; 36 | --bg-y: 0%; 37 | --bg-x: 0%; 38 | --radius: 16px; 39 | --duration: 500ms; 40 | transition: transform cubic-bezier(0.075, 0.82, 0.165, 1) var(--duration); 41 | } 42 | 43 | .hover-card-layer { 44 | z-index: -1; 45 | mix-blend-mode: color-dodge; 46 | opacity: var(--opacity); 47 | will-change: background; 48 | transition: cubic-bezier(0.075, 0.82, 0.165, 1) var(--duration); 49 | transition-property: opacity; 50 | clip-path: inset(0 0 1px 0 round var(--radius)); 51 | --foil-svg: url('data:image/svg+xml,'); 52 | --step: 5%; 53 | --foil-size: 50%; 54 | --pattern: var(--foil-svg) center/100% no-repeat; 55 | --rainbow: repeating-linear-gradient( 56 | 0deg, 57 | rgb(255, 119, 115) calc(var(--step) * 1), 58 | rgba(255, 237, 95, 1) calc(var(--step) * 2), 59 | rgba(168, 255, 95, 1) calc(var(--step) * 3), 60 | rgba(131, 255, 247, 1) calc(var(--step) * 4), 61 | rgba(120, 148, 255, 1) calc(var(--step) * 5), 62 | rgb(216, 117, 255) calc(var(--step) * 6), 63 | rgb(255, 119, 115) calc(var(--step) * 7) 64 | ) 65 | 0% var(--bg-y) / 200% 700% no-repeat; 66 | --diagonal: repeating-linear-gradient( 67 | 128deg, 68 | #0e152e 0%, 69 | hsl(180, 10%, 60%) 3.8%, 70 | hsl(180, 10%, 60%) 4.5%, 71 | hsl(180, 10%, 60%) 5.2%, 72 | #0e152e 10%, 73 | #0e152e 12% 74 | ) 75 | var(--bg-x) var(--bg-y) / 300% no-repeat; 76 | --shade: radial-gradient( 77 | farthest-corner circle at var(--m-x) var(--m-y), 78 | rgba(255, 255, 255, 0.1) 12%, 79 | rgba(255, 255, 255, 0.15) 20%, 80 | rgba(255, 255, 255, 0.25) 120% 81 | ) 82 | var(--bg-x) var(--bg-y) / 300% no-repeat; 83 | background-blend-mode: hue, hue, hue, overlay; 84 | background: var(--pattern), var(--rainbow), var(--diagonal), var(--shade); 85 | } 86 | 87 | .hover-card-layer::after { 88 | content: ""; 89 | position: absolute; 90 | inset: 0; 91 | grid-area: inherit; 92 | background-image: inherit; 93 | background-repeat: inherit; 94 | background-attachment: inherit; 95 | background-origin: inherit; 96 | background-clip: inherit; 97 | background-color: inherit; 98 | mix-blend-mode: exclusion; 99 | background-size: var(--foil-size), 200% 400%, 800%, 200%; 100 | background-position: center, 0% var(--bg-y), 101 | calc(var(--bg-x) * -1) calc(var(--bg-y) * -1), var(--bg-x) var(--bg-y); 102 | background-blend-mode: soft-light, hue, hard-light; 103 | } 104 | -------------------------------------------------------------------------------- /app/layout.client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Nav } from "@/components/nav"; 3 | import { cn } from "@/lib/cn"; 4 | import { usePathname } from "next/navigation"; 5 | 6 | export function Main({ children }: { children: React.ReactNode }) { 7 | const pathname = usePathname(); 8 | const fullScreen = 9 | pathname === "/playground" || pathname.startsWith("/playground"); 10 | 11 | return ( 12 |
13 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from "next/font/google"; 2 | import "./globals.css"; 3 | import { cn } from "@/lib/cn"; 4 | import { createMetadata } from "@/lib/metadata"; 5 | import { Main } from "./layout.client"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata = createMetadata({ 10 | title: { 11 | absolute: "Fuma Nama", 12 | template: "Fuma Nama | %s", 13 | }, 14 | description: "My personal website.", 15 | }); 16 | 17 | export default function RootLayout({ 18 | children, 19 | }: Readonly<{ 20 | children: React.ReactNode; 21 | }>) { 22 | return ( 23 | 24 | 30 |
{children}
31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |

Fuma Nama

7 | 8 |

9 | Hello there, I am an open sourcerer from Hong Kong. Currently involved 10 | in software development & web design, passionated on a wide spectrum of 11 | topics. 12 |
13 |
14 | Besides, I love the joy of creating content, art, and the formation of 15 | interactive, graceful websites. During my leisure, I also investigate in 16 | artificial intelligence technologies. Having fun in coding! 17 |
18 |
19 | Creator of{" "} 20 | 24 | Fumadocs 25 | 26 | , a Next.js lover, an anime maniac, and contributor of many libraries I 27 | love. 28 |

29 |
30 | 31 | 32 | GitHub 33 | 37 | 38 | Github 39 | 40 | 41 | 42 | X 43 | 47 | 48 | Twitter 49 | 50 |
51 |
52 | ); 53 | } 54 | 55 | function Badge({ href, children }: { href: string; children: ReactNode }) { 56 | return ( 57 | 62 | {children} 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /app/playground/demo/api.ts: -------------------------------------------------------------------------------- 1 | export interface RankingResponse { 2 | data: { node: Anime }[]; 3 | } 4 | 5 | export interface Anime { 6 | id: number; 7 | title: string; 8 | main_picture: { 9 | medium: string; 10 | large: string; 11 | }; 12 | 13 | studios?: { id: string; name: string }[]; 14 | } 15 | 16 | export async function getAnimeRank(): Promise { 17 | const response = await fetch( 18 | "https://api.myanimelist.net/v2/anime/ranking?ranking_type=all&fields=studios", 19 | { 20 | headers: { 21 | "X-MAL-CLIENT-ID": process.env.MAL_CLIENT_ID!, 22 | }, 23 | } 24 | ); 25 | 26 | return (await response.json()) as RankingResponse; 27 | } 28 | -------------------------------------------------------------------------------- /app/playground/demo/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExitBar, Shell } from "@/components/ui/3d-shell"; 2 | import { ScrollArea } from "@/components/ui/scroll-area"; 3 | import { cn } from "@/lib/cn"; 4 | import { cva } from "class-variance-authority"; 5 | import { type Anime, getAnimeRank } from "./api"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center gap-1.5 p-2 text-start rounded-xl transition-colors text-xs [&_svg]:size-4 [&_svg]:flex-shrink-0", 9 | { 10 | variants: { 11 | active: { 12 | true: "bg-neutral-50/20 font-medium", 13 | false: "text-neutral-100/70 hover:bg-neutral-50/10", 14 | }, 15 | }, 16 | } 17 | ); 18 | 19 | export default async function Page() { 20 | const ranking = await getAnimeRank(); 21 | 22 | return ( 23 | <> 24 | 25 | 26 | 27 |

Trending

28 |

29 | {ranking.data.length} series. 30 |

31 | 32 |
33 | 44 | 45 | 46 | 47 | 51 |
52 | 53 |
54 | {ranking.data.map((anime) => ( 55 | 56 | ))} 57 |
58 |
59 |
60 | 61 | 62 | ); 63 | } 64 | 65 | function AnimeItem({ anime }: { anime: Anime }) { 66 | return ( 67 |
68 |
69 | Picture 74 |

{anime.title}

75 |

76 | {anime.studios?.map((s) => s.name).join()} 77 |

78 |
79 | ); 80 | } 81 | 82 | function Sidebar() { 83 | const playlists = [ 84 | { 85 | name: "Romance", 86 | color: "rgb(225,100,100)", 87 | }, 88 | { 89 | name: "Actions", 90 | color: "rgb(100,100,225)", 91 | }, 92 | ]; 93 | 94 | return ( 95 |
96 |

97 | Anime 98 | 109 | 110 | 111 | 112 | 113 |

114 | 130 | 145 | 161 |

Playlists

162 | 180 | {playlists.map((p) => ( 181 | 195 | ))} 196 |
197 | ); 198 | } 199 | -------------------------------------------------------------------------------- /app/playground/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ 2 | children, 3 | }: Readonly<{ 4 | children: React.ReactNode; 5 | }>) { 6 | return ( 7 |
15 | {children} 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/playground/math/page.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from "@/components/canvas/math"; 2 | import { ExitBar, Shell } from "@/components/ui/3d-shell"; 3 | 4 | export default function Page() { 5 | const functions = [ 6 | { 7 | name: "sin", 8 | color: "rgba(255,50,50,0.8)", 9 | }, 10 | { 11 | name: "cos", 12 | color: "rgba(100,200,255,0.8)", 13 | }, 14 | { 15 | name: "tan", 16 | color: "rgba(100,255,100,0.8)", 17 | }, 18 | ]; 19 | 20 | return ( 21 | <> 22 | 23 |
24 | {functions.map((f) => ( 25 |
29 | 37 | 38 | 39 |

{f.name}

40 |
41 | ))} 42 |
43 | 44 |
45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /app/playground/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | 5 | const apps = [ 6 | { 7 | name: "WebGL", 8 | url: "/playground/webgl", 9 | }, 10 | { 11 | name: "Anime", 12 | url: "/playground/demo", 13 | }, 14 | { 15 | name: "Math", 16 | url: "/playground/math", 17 | }, 18 | { 19 | name: "Physic", 20 | url: "/playground/physic", 21 | }, 22 | ]; 23 | 24 | export default function Page() { 25 | return ( 26 | <> 27 |
28 | {apps.map((app) => ( 29 | 39 | {app.name} 40 | 41 | ))} 42 |
43 |

44 | Some boring things I've created for fun. 45 |

46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /app/playground/physic/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ExitBar, Shell } from "@/components/ui/3d-shell"; 4 | import { useEffect, useRef } from "react"; 5 | 6 | export default function Page() { 7 | const canvasRef = useRef(null); 8 | 9 | useEffect(() => { 10 | const element = canvasRef.current; 11 | 12 | if (!element) return; 13 | const renderer = createRenderer({ canvas: element }); 14 | 15 | return () => { 16 | renderer(); 17 | }; 18 | }, []); 19 | 20 | return ( 21 | <> 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | function createRenderer({ canvas }: { canvas: HTMLCanvasElement }) { 31 | let unmounted = false; 32 | let speedX = 1.2, 33 | speedY = -2; 34 | let x = 100, 35 | y = 100, 36 | r = 50; 37 | let mouseX = -1, 38 | mouseY = -1; 39 | const ctx = canvas.getContext("2d")!; 40 | 41 | function onMouseMove(event: MouseEvent) { 42 | const rect = canvas.getBoundingClientRect(); 43 | if ( 44 | event.clientX < rect.left || 45 | event.clientX > rect.right || 46 | event.clientY < rect.top || 47 | event.clientY > rect.bottom 48 | ) { 49 | mouseX = -1; 50 | mouseY = -1; 51 | } else { 52 | mouseX = event.clientX - rect.left; 53 | mouseY = event.clientY - rect.top; 54 | } 55 | } 56 | 57 | let lastRender = Date.now(); 58 | function render() { 59 | if (unmounted) return; 60 | 61 | if ( 62 | canvas.width !== canvas.clientWidth * window.devicePixelRatio || 63 | canvas.height !== canvas.clientHeight * window.devicePixelRatio 64 | ) { 65 | canvas.width = canvas.clientWidth * window.devicePixelRatio; 66 | canvas.height = canvas.clientHeight * window.devicePixelRatio; 67 | ctx.scale(window.devicePixelRatio, window.devicePixelRatio); 68 | } 69 | 70 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight); 71 | 72 | const now = Date.now(); 73 | const dt = (now - lastRender) / 1000; 74 | 75 | if ( 76 | (x <= 0 && speedX < 0) || 77 | (x + 2 * r >= canvas.clientWidth && speedX > 0) 78 | ) { 79 | speedX = Math.abs(speedX) >= 1 ? -speedX * 0.5 : 0; 80 | } 81 | 82 | if ( 83 | (y <= 0 && speedY < 0) || 84 | (y + 2 * r >= canvas.clientHeight && speedY > 0) 85 | ) { 86 | speedY = Math.abs(speedY) >= 1 ? -speedY * 0.5 : 0; 87 | } 88 | 89 | if (mouseX === -1 && mouseY === -1) { 90 | // return to natural gravity 91 | if (y + 2 * r < canvas.clientHeight) speedY += 9.81 * dt; 92 | } else { 93 | const dx = canvas.clientWidth / 2 - mouseX; 94 | const dy = canvas.clientHeight / 2 - mouseY; 95 | const d = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); 96 | 97 | if ((dx > 0 && x + 2 * r < canvas.clientWidth) || (dx < 0 && x > 0)) 98 | speedX += 9.81 * (dx / d) * dt; 99 | if ((dy > 0 && y + 2 * r < canvas.clientHeight) || (dy < 0 && y > 0)) { 100 | speedY += 9.81 * (dy / d) * dt; 101 | } 102 | 103 | ctx.beginPath(); 104 | ctx.arc(mouseX, mouseY, 10, 0, 2 * Math.PI); 105 | ctx.fillStyle = "rgb(200,225,255)"; 106 | ctx.fill(); 107 | } 108 | 109 | // Assume 1m = 10px 110 | x = speedX * 10 + x; 111 | y = speedY * 10 + y; 112 | x = Math.min(Math.max(x, 0), canvas.clientWidth - 2 * r); 113 | y = Math.min(Math.max(y, 0), canvas.clientHeight - 2 * r); 114 | 115 | ctx.filter = "none"; 116 | ctx.beginPath(); 117 | ctx.arc(x + r, y + r, r, 0, 2 * Math.PI); 118 | ctx.fillStyle = "white"; 119 | ctx.fill(); 120 | 121 | lastRender = now; 122 | requestAnimationFrame(() => render()); 123 | } 124 | 125 | render(); 126 | document.addEventListener("mousemove", onMouseMove); 127 | 128 | return () => { 129 | document.removeEventListener("mousemove", onMouseMove); 130 | unmounted = true; 131 | }; 132 | } 133 | -------------------------------------------------------------------------------- /app/playground/webgl/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExitBar, Shell } from "@/components/ui/3d-shell"; 2 | import { Canvas } from "@/components/canvas/webgl"; 3 | 4 | export default function Page() { 5 | return ( 6 | <> 7 | 8 | 9 | 15 | @stormoid 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/projects/page.tsx: -------------------------------------------------------------------------------- 1 | import { createMetadata } from "@/lib/metadata"; 2 | 3 | export const metadata = createMetadata({ 4 | title: "Projects", 5 | description: "My proud, high-quality treasures.", 6 | }); 7 | 8 | const projects: (ProjectProps & { tag: string })[] = [ 9 | { 10 | tag: "project", 11 | name: "Fumadocs", 12 | description: 13 | "The powerful framework for building documentation sites in Next.js.", 14 | href: "https://fumadocs.vercel.app", 15 | }, 16 | { 17 | tag: "project", 18 | name: "Shark Chat", 19 | description: "A modern chat app.", 20 | href: "https://shark-chat.vercel.app", 21 | }, 22 | { 23 | tag: "project", 24 | name: "Fuma Comment", 25 | description: "A comment area for your blog.", 26 | href: "https://fuma-comment.vercel.app", 27 | }, 28 | { 29 | tag: "project", 30 | name: "Fuma Lofi", 31 | description: "Some nice Lofi music and a music player.", 32 | href: "https://fuma-lofi.vercel.app", 33 | }, 34 | { 35 | tag: "project", 36 | name: "next-validate-link", 37 | description: 38 | "An utility to validate links in your Markdown files.", 39 | href: "https://next-validate-link.vercel.app", 40 | }, 41 | { 42 | tag: "project", 43 | name: "Fuma Content", 44 | description: "A library for handling content.", 45 | href: "https://fuma-content.vercel.app", 46 | }, 47 | { 48 | tag: "project", 49 | name: "Discord FP", 50 | description: 51 | "A Beautiful Application Command Library for Discord.js and Discordeno.", 52 | href: "https://github.com/fuma-nama/discord-fp", 53 | }, 54 | { 55 | tag: "toy", 56 | name: "Kanji Animated", 57 | description: 58 | "Animation inspired by 命に嫌われている MV (by Iori Kanzaki), based on Canvas API.", 59 | href: "https://kanji-animated.vercel.app", 60 | }, 61 | { 62 | tag: "toy", 63 | name: "No Deploy", 64 | description: "Robust and scalable hosting platform that supports nothing.", 65 | href: "https://nodeploy-neon.vercel.app", 66 | }, 67 | { 68 | tag: "toy", 69 | name: "Shot on Stone", 70 | description: "View your photo carved on a stone.", 71 | href: "https://shot-on-stone.vercel.app", 72 | }, 73 | { 74 | tag: "toy", 75 | name: "Fuma Space", 76 | description: "My little project exploring Nuxt.js and Vue.js", 77 | href: "https://fuma-space.vercel.app", 78 | }, 79 | { 80 | tag: "toy", 81 | name: "Simple Game", 82 | description: "A thing to waste your time.", 83 | href: "https://simple-game-pi.vercel.app", 84 | }, 85 | { 86 | tag: "toy", 87 | name: "Astro Blog", 88 | description: "An example blog using Astro", 89 | href: "https://fuma-blog.vercel.app", 90 | }, 91 | { 92 | tag: "toy", 93 | name: "No Deploy CLI", 94 | description: "A CLI tool for No Deploy written in Rust.", 95 | href: "https://github.com/fuma-nama/nodeploy-cli", 96 | }, 97 | { 98 | tag: "experimental", 99 | name: "Discord Bot Template", 100 | description: "A Discord bot template with dashboard and documentation.", 101 | href: "https://github.com/fuma-nama/discord-bot-template", 102 | }, 103 | { 104 | tag: "legacy", 105 | name: "Old Portfolio", 106 | description: "My outdated yet well-designed portfolio.", 107 | href: "https://money-portfolio.vercel.app/", 108 | }, 109 | { 110 | tag: "legacy", 111 | name: "JDAK", 112 | description: 113 | "A fast, flexible command framework for JDA written in Kotlin.", 114 | href: "https://github.com/fuma-nama/jdak", 115 | }, 116 | { 117 | tag: "legacy", 118 | name: "Omagize", 119 | description: 120 | "A modern and powerful web chat app written in Java & TypeScript.", 121 | href: "https://github.com/fuma-nama/omagize", 122 | }, 123 | { 124 | tag: "legacy", 125 | name: "Discord UI", 126 | description: "Write user interfaces for Discord bot.", 127 | href: "https://github.com/fuma-nama/discord-ui", 128 | }, 129 | ]; 130 | 131 | export default function Page() { 132 | return ( 133 |
134 |

Projects

135 |

136 | Nice frameworks, libraries, web apps. My proud, high-quality harvests. 137 |

138 |
139 | {projects 140 | .filter((p) => p.tag === "project") 141 | .map((p) => ( 142 | 143 | ))} 144 |
145 | 146 |

My Toys

147 |

Fun projects.

148 |
149 | {projects 150 | .filter((p) => p.tag === "toy") 151 | .map((p) => ( 152 | 153 | ))} 154 |
155 | 156 |

Experimental

157 |

158 | Bleeding edge stuffs for testing purposes. 159 |

160 |
161 | {projects 162 | .filter((p) => p.tag === "experimental") 163 | .map((p) => ( 164 | 165 | ))} 166 |
167 |

Legacy

168 |

169 | My neglected, abandoned projects. 170 |

171 |
172 | {projects 173 | .filter((p) => p.tag === "legacy") 174 | .map((p) => ( 175 | 176 | ))} 177 |
178 |
179 | ); 180 | } 181 | 182 | interface ProjectProps { 183 | name: string; 184 | description: string; 185 | href: string; 186 | } 187 | 188 | function Project(project: ProjectProps) { 189 | return ( 190 | 195 |

{project.name}

196 |

{project.description}

197 |
198 | ); 199 | } 200 | -------------------------------------------------------------------------------- /app/rss.xml/route.ts: -------------------------------------------------------------------------------- 1 | import { Feed } from "feed"; 2 | import { documents } from "../source"; 3 | import { NextResponse } from "next/server"; 4 | export const revalidate = false; 5 | 6 | export function GET() { 7 | const feed = createFeed(); 8 | 9 | return new NextResponse(feed.rss2(), { 10 | headers: { 11 | "Content-Type": "application/xml", 12 | }, 13 | }); 14 | } 15 | 16 | function createFeed(): Feed { 17 | const site_url = "https://fuma-nama.vercel.app"; 18 | 19 | const feed = new Feed({ 20 | id: "fuma-nama", 21 | title: "Fuma Nama", 22 | description: "Fuma Nama's blog", 23 | language: "en", 24 | copyright: `All rights reserved ${new Date().getFullYear()}, Fuma Nama`, 25 | image: `${site_url}/banner.png`, 26 | link: site_url, 27 | }); 28 | 29 | for (const post of documents) { 30 | feed.addItem({ 31 | id: post.id, 32 | link: `${site_url}/blog/${post.id}`, 33 | title: post.info.title, 34 | description: post.info.description, 35 | author: [{ name: "Fuma Nama" }], 36 | date: post.info.date, 37 | }); 38 | } 39 | 40 | return feed; 41 | } 42 | -------------------------------------------------------------------------------- /app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:url"; 2 | import { MetadataRoute } from "next"; 3 | import { documents } from "./source"; 4 | import { baseUrl } from "@/lib/metadata"; 5 | 6 | export default function sitemap(): MetadataRoute.Sitemap { 7 | const getUrl = (v: string) => resolve(baseUrl, v); 8 | 9 | return [ 10 | { 11 | url: getUrl("/"), 12 | lastModified: new Date(), 13 | changeFrequency: "monthly", 14 | priority: 1, 15 | }, 16 | { 17 | url: getUrl("/projects"), 18 | lastModified: new Date(), 19 | changeFrequency: "monthly", 20 | priority: 0.8, 21 | }, 22 | { 23 | url: getUrl("/blog"), 24 | lastModified: new Date(), 25 | changeFrequency: "weekly", 26 | priority: 0.5, 27 | }, 28 | ...documents.map((d) => ({ 29 | url: getUrl(`/blog/${d.id}`), 30 | lastModified: d.info.date, 31 | changeFrequency: "weekly", 32 | priority: 0.5, 33 | })), 34 | ]; 35 | } 36 | -------------------------------------------------------------------------------- /app/source.ts: -------------------------------------------------------------------------------- 1 | import entry from "content"; 2 | import { document } from "fuma-content"; 3 | import * as path from "node:path"; 4 | 5 | export const documents = document(entry) 6 | .map((d) => ({ 7 | id: path.basename(d.file, path.extname(d.file)), 8 | ...d, 9 | info: d.info as { title: string; description: string; date: Date }, 10 | })) 11 | .sort((a, b) => b.info.date.getTime() - a.info.date.getTime()); 12 | -------------------------------------------------------------------------------- /auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import GitHub from "next-auth/providers/github"; 3 | import { PrismaAdapter } from "@auth/prisma-adapter"; 4 | import { prisma } from "./lib/prisma"; 5 | 6 | export const { auth, handlers, signIn, signOut } = NextAuth({ 7 | adapter: PrismaAdapter(prisma), 8 | providers: [GitHub], 9 | }); 10 | -------------------------------------------------------------------------------- /components/canvas/math.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRef, useEffect } from "react"; 3 | 4 | export function Canvas() { 5 | const ref = useRef(null); 6 | 7 | useEffect(() => { 8 | if (!ref.current) return; 9 | 10 | let unmounted = false; 11 | let animation = false; 12 | const element = ref.current!; 13 | const ctx = element.getContext("2d")!; 14 | const width = element.clientWidth, 15 | height = element.clientHeight; 16 | 17 | element.width = width * window.devicePixelRatio; 18 | element.height = height * window.devicePixelRatio; 19 | ctx.scale(window.devicePixelRatio, window.devicePixelRatio); 20 | 21 | function render() { 22 | ctx.clearRect(0, 0, width, height); 23 | 24 | ctx.beginPath(); 25 | ctx.moveTo(0, height / 2); 26 | ctx.lineTo(width, height / 2); 27 | ctx.strokeStyle = "rgba(255,255,255,0.5)"; 28 | ctx.stroke(); 29 | 30 | ctx.strokeStyle = "rgba(255,50,50,0.8)"; 31 | line((x) => Math.sin(x * 20)); 32 | 33 | ctx.strokeStyle = "rgba(100,200,255,0.8)"; 34 | line((x) => Math.cos(x * 20)); 35 | 36 | ctx.strokeStyle = "rgba(100,255,100,0.8)"; 37 | line((x) => Math.tan(x * 20)); 38 | 39 | if (!unmounted && animation) requestAnimationFrame(() => render()); 40 | } 41 | 42 | function line(fn: (x: number) => number) { 43 | ctx.beginPath(); 44 | 45 | for (let i = 0; i <= width; i++) { 46 | const x = i / width; 47 | const y = (height - (fn(x) * height) / 4) / 2; 48 | 49 | if (Number.isNaN(y) || y > height || y < -height) { 50 | ctx.moveTo(i, y); 51 | continue; 52 | } 53 | 54 | ctx.lineTo(i, y); 55 | } 56 | 57 | ctx.lineWidth = 2; 58 | ctx.stroke(); 59 | } 60 | 61 | render(); 62 | return () => { 63 | unmounted = true; 64 | }; 65 | }, []); 66 | 67 | return ; 68 | } 69 | -------------------------------------------------------------------------------- /components/canvas/trigonometry.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect, useRef } from "react"; 3 | 4 | export function Canvas() { 5 | const ref = useRef(null); 6 | 7 | useEffect(() => { 8 | if (!ref.current) return; 9 | 10 | const renderer = createRenderer(ref.current); 11 | 12 | return () => { 13 | renderer(); 14 | }; 15 | }); 16 | 17 | return ( 18 | 24 | ); 25 | } 26 | 27 | function createRenderer(canvas: HTMLCanvasElement) { 28 | const ctx = canvas.getContext("2d")!; 29 | 30 | let unmounted = false; 31 | let th = Math.PI / 4; 32 | const speed = 0.5; 33 | const r = 150; 34 | 35 | function render() { 36 | if (unmounted) return; 37 | 38 | th = (th + (Math.PI / 180) * speed) % (2 * Math.PI); 39 | const w = Math.cos(th) * r; 40 | const h = Math.sin(th) * r; 41 | 42 | ctx.clearRect(0, 0, canvas.width, canvas.height); 43 | 44 | ctx.translate(canvas.width / 2, canvas.height / 2); 45 | ctx.beginPath(); 46 | ctx.moveTo(0, 0); 47 | ctx.lineTo(w, 0); 48 | ctx.lineTo(w, -h); 49 | ctx.lineTo(0, 0); 50 | ctx.strokeStyle = "white"; 51 | ctx.lineWidth = 2; 52 | ctx.stroke(); 53 | 54 | ctx.beginPath(); 55 | ctx.arc(0, 0, r, 0, 2 * Math.PI); 56 | ctx.strokeStyle = "rgba(255,255,255,0.5)"; 57 | ctx.stroke(); 58 | 59 | ctx.beginPath(); 60 | ctx.moveTo(0, -r); 61 | ctx.lineTo(0, r); 62 | ctx.moveTo(-r, 0); 63 | ctx.lineTo(r, 0); 64 | ctx.strokeStyle = "rgba(255,255,255,0.3)"; 65 | ctx.stroke(); 66 | ctx.resetTransform(); 67 | 68 | requestAnimationFrame(() => render()); 69 | } 70 | 71 | render(); 72 | 73 | return () => { 74 | unmounted = true; 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /components/canvas/webgl.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { HTMLAttributes, useEffect, useRef } from "react"; 3 | import Phenomenon from "phenomenon"; 4 | 5 | export function Canvas(props: HTMLAttributes) { 6 | const ref = useRef(null); 7 | 8 | useEffect(() => { 9 | const element = ref.current; 10 | if (!element) return; 11 | const renderer = createRenderer(element); 12 | 13 | return () => { 14 | renderer.destroy(); 15 | }; 16 | }); 17 | 18 | return ; 19 | } 20 | 21 | type InstanceProps = Phenomenon["add"] extends (a: any, props: infer P) => any 22 | ? P 23 | : never; 24 | type ExtendedInstanceProps = InstanceProps & { 25 | onRender: (instance: Instance) => void; 26 | }; 27 | type Instance = ReturnType; 28 | 29 | const vertex = ` 30 | precision mediump float; 31 | attribute vec3 aPosition; 32 | 33 | void main(){ 34 | gl_Position = vec4(aPosition, 1.); 35 | } 36 | `; 37 | 38 | // Ether by nimitz 2014 (twitter: @stormoid) 39 | // https://www.shadertoy.com/view/MsjSW3 40 | // License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License 41 | 42 | const fragment = ` 43 | precision mediump float; 44 | uniform float u_time; 45 | uniform vec2 u_resolution; 46 | 47 | mat2 m(float a){ 48 | float c=cos(a); 49 | float s=sin(a); 50 | return mat2(c,-s,s,c); 51 | } 52 | 53 | float map(vec3 p){ 54 | p.xz*= m(u_time*0.4);p.xy*= m(u_time*0.3); 55 | vec3 q = p*2.+u_time; 56 | return length(p+vec3(sin(u_time*0.7)))*log(length(p)+1.) + sin(q.x+sin(q.z+sin(q.y)))*0.5 - 1.; 57 | } 58 | 59 | void main(){ 60 | vec2 p = gl_FragCoord.xy/u_resolution.y - vec2((u_resolution.x/2.0 - 2.5)/u_resolution.y,.5); 61 | vec3 cl = vec3(0.); 62 | float d = 2.5; 63 | for(int i=0; i<=5; i++) { 64 | vec3 p = vec3(0,0,5.) + normalize(vec3(p, -1.))*d; 65 | float rz = map(p); 66 | float f = clamp((rz - map(p+.1))*0.5, -.1, 1. ); 67 | vec3 l = vec3(0.1,0.3,.4) + vec3(5., 2.5, 3.)*f; 68 | cl = cl*l + smoothstep(2.5, .0, rz)*.7*l; 69 | d += min(rz, 1.); 70 | } 71 | 72 | gl_FragColor = vec4(cl, 1.); 73 | } 74 | `; 75 | 76 | export function createRenderer(canvas: HTMLCanvasElement): Phenomenon { 77 | const renderer = new Phenomenon({ 78 | canvas, 79 | settings: { 80 | alpha: true, 81 | position: { x: 0, y: 0, z: 1 }, 82 | shouldRender: true, 83 | }, 84 | }); 85 | 86 | renderer.add("starling", { 87 | mode: 4, 88 | vertex, 89 | geometry: { 90 | vertices: [ 91 | { x: -100, y: 100, z: 0 }, 92 | { x: -100, y: -100, z: 0 }, 93 | { x: 100, y: 100, z: 0 }, 94 | { x: 100, y: -100, z: 0 }, 95 | { x: -100, y: -100, z: 0 }, 96 | { x: 100, y: 100, z: 0 }, 97 | ] as unknown as object[][], 98 | }, 99 | fragment, 100 | uniforms: { 101 | u_resolution: { 102 | type: "vec2", 103 | value: [canvas.width, canvas.height], 104 | }, 105 | u_time: { 106 | type: "float", 107 | value: [0.0], 108 | }, 109 | }, 110 | onRender: ({ uniforms }) => { 111 | uniforms.u_time.value[0] += 0.01; 112 | }, 113 | } as ExtendedInstanceProps); 114 | 115 | return renderer; 116 | } 117 | -------------------------------------------------------------------------------- /components/hover-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRef } from "react"; 3 | 4 | export function HoverCard() { 5 | const ref = useRef(null); 6 | const containerRef = useRef(null); 7 | 8 | return ( 9 |
{ 15 | const element = ref.current; 16 | if (!element) return; 17 | 18 | const bounding = element.getBoundingClientRect(); 19 | const centerX = bounding.left + bounding.width / 2; 20 | const centerY = bounding.top + bounding.height / 2; 21 | element.style.setProperty( 22 | "transform", 23 | `rotateY(${ 24 | ((e.clientX - centerX) / bounding.width) * -2 25 | }deg) rotateX(${((e.clientY - centerY) / bounding.height) * 2}deg)` 26 | ); 27 | element.style.setProperty("--opacity", "0.6"); 28 | element.style.setProperty( 29 | "--bg-x", 30 | `${100 - ((e.clientX - bounding.left) / bounding.width) * 10}%` 31 | ); 32 | element.style.setProperty( 33 | "--bg-y", 34 | `${100 - ((e.clientY - bounding.top) / bounding.height) * 10}%` 35 | ); 36 | element.style.setProperty( 37 | "--m-x", 38 | `${((e.clientX - bounding.left) / bounding.width) * 100}%` 39 | ); 40 | element.style.setProperty( 41 | "--m-y", 42 | `${((e.clientY - bounding.top) / bounding.height) * 100}%` 43 | ); 44 | }} 45 | onMouseLeave={() => { 46 | const element = ref.current; 47 | if (!element) return; 48 | 49 | element.style.setProperty("transform", "none"); 50 | element.style.setProperty("--opacity", "0"); 51 | }} 52 | > 53 |
57 |
61 |

Hello World

62 |

63 | Hovering this card will give you an impressing experience. This card 64 | was inspired by Linear's customer page, their design is very inspiring 65 | and wonderful. 66 |

67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /components/music-banner.tsx: -------------------------------------------------------------------------------- 1 | import Eve from "@/public/eve.jpg"; 2 | import Iori from "@/public/iori-kanzaki.jpg"; 3 | import Yorushika from "@/public/yorushika.jpg"; 4 | import Image from "next/image"; 5 | 6 | export function MusicBanner() { 7 | return ( 8 |
9 | iori 14 | eve 19 | Yorushika 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn } from "@/lib/cn"; 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | import { useEffect, useRef, useState } from "react"; 7 | 8 | export function Nav() { 9 | return ( 10 | 24 | ); 25 | } 26 | 27 | export function NavLink({ 28 | href, 29 | children, 30 | }: { 31 | href: string; 32 | children: React.ReactNode; 33 | }) { 34 | const pathname = usePathname(); 35 | const active = pathname === href || pathname.startsWith(href + "/"); 36 | const linkRef = useRef(null); 37 | const [width, setWidth] = useState("100%"); 38 | 39 | useEffect(() => { 40 | if (!linkRef.current) return; 41 | 42 | setWidth(linkRef.current.clientWidth); 43 | }, []); 44 | 45 | return ( 46 | 54 | {children} 55 |
63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /components/spotify.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | export { Spotify } from "react-spotify-embed"; 3 | -------------------------------------------------------------------------------- /components/svg-components.tsx: -------------------------------------------------------------------------------- 1 | export function SVGButton() { 2 | return ( 3 | 49 | ); 50 | } 51 | 52 | export function SVGCircle() { 53 | return ( 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 88 | 95 | 102 | 103 | 104 | 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /components/ui/3d-shell.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/cn"; 4 | import Link from "next/link"; 5 | import { HTMLAttributes, useEffect, useId, useRef } from "react"; 6 | 7 | export function Shell({ 8 | children, 9 | className, 10 | ...props 11 | }: HTMLAttributes) { 12 | const boxRef = useRef(null); 13 | 14 | useEffect(() => { 15 | if (!boxRef.current) return; 16 | const box = boxRef.current; 17 | 18 | const listener = (e: MouseEvent) => { 19 | const bounds = box.getBoundingClientRect(); 20 | const leftX = e.clientX - bounds.x; 21 | const topY = e.clientY - bounds.y; 22 | const centerX = leftX - bounds.width / 2; 23 | const centerY = topY - bounds.height / 2; 24 | 25 | const distance = Math.sqrt(centerX ** 2 + centerY ** 2); 26 | 27 | box.style.setProperty("transition", `none`); 28 | box.style.setProperty( 29 | "background-image", 30 | `radial-gradient( 31 | circle at 32 | ${centerX * 2 + bounds.width / 2}px 33 | ${centerY * 2 + bounds.height / 2}px, 34 | rgba(145,145,185,0.3), 35 | transparent 50%)` 36 | ); 37 | box.style.setProperty( 38 | "transform", 39 | `rotate3d( 40 | ${centerY / 100}, 41 | ${-centerX / 100}, 42 | 0, 43 | ${Math.log(distance) * 1.5}deg)` 44 | ); 45 | }; 46 | 47 | const onLeave = () => { 48 | box.style.setProperty("transition", `transform 1s`); 49 | box.style.setProperty("background-image", `none`); 50 | box.style.setProperty("transform", `none`); 51 | }; 52 | 53 | box.addEventListener("mousemove", listener); 54 | box.addEventListener("mouseleave", onLeave); 55 | 56 | return () => { 57 | box.removeEventListener("mousemove", listener); 58 | box.removeEventListener("mouseleave", onLeave); 59 | }; 60 | }, []); 61 | 62 | return ( 63 |
71 |
78 | {children} 79 |
80 | ); 81 | } 82 | 83 | export function ExitBar() { 84 | return ( 85 | 90 |
91 |
92 | 93 | ); 94 | } 95 | 96 | export function Border({ color }: { color: string[] }) { 97 | const id = useId(); 98 | 99 | return ( 100 | 101 | 102 | {color.map((c, i) => ( 103 | 104 | ))} 105 | 106 | 107 | 108 | 109 | 110 | 120 | 121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn } from "@/lib/cn"; 3 | import * as Primitive from "@radix-ui/react-scroll-area"; 4 | 5 | export function ScrollArea( 6 | props: React.ComponentPropsWithoutRef 7 | ) { 8 | return ( 9 | 10 | 11 | {props.children} 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | function Scrollbar({ 19 | className, 20 | orientation = "vertical", 21 | ...props 22 | }: React.ComponentPropsWithRef) { 23 | return ( 24 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /components/youtube.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export { default } from "react-lite-youtube-embed"; 4 | -------------------------------------------------------------------------------- /content/about-html.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: HTML 3 | description: Why HTML? 4 | date: 2024-05-21 5 | --- 6 | 7 | ## HTMl 8 | 9 | About the essence of **HTML** and **DOM tree**, I honestly think it is a bad design in modern web. 10 | It does something different from native apps, and produces a lot of **redundant work**. 11 | 12 | ### JavaScript Bias 13 | 14 | Why do we need to avoid scripts that much in web apps? 15 | 16 | We developed **Hydration**, **Server-Side Rendering**, and a bunch of things to reduce the bundle size of JavaScript on client-side. 17 | The reason why bundle size matters, is that it becomes heavy at some point of our development, and slows down the loading speed. 18 | 19 | **Many APIs are offered by browsers:** UI, Audio APIs... Why is it heavy? 20 | 21 | The reason is simple: How could you implement a popover component? Yes, you will go ahead and search for NPM modules that solves the problem. 22 | **These packages increased your client bundle size**, but why the Popover API is arriving so late? 23 | 24 | As of 2024 May, Popover is yet an experimental feature and has inconsistency problems. 25 | **The problem turns out, to be a problem of HTML.** 26 | 27 | ### Lack of Flexibility 28 | 29 | In HTML, We cannot make a proper implementation of complex, advanced components without **hacky ways**. 30 | 31 | Like **Rich text editors, color pickers, checkbox with custom icons, customised select menus**, we ultimately _fixed_ it with JavaScript and some magic. 32 | Why aren't they being offered by browsers? We obviously needed a better select menu that is more flexible, a more advanced input field with Rich text support. 33 | 34 | Accessing the UI through DOM tree, **there are very few options to customise the elements**. 35 | CSS as a styling language, can't be as flexible as JavaScript. 36 | We cannot customise the dropdown menu of ` 31 | 32 |
33 | 36 | 42 |
43 | 44 |
45 | 46 | 47 |
48 |
49 | 50 | Can you find any problems? Yes, we can find: 51 | 52 | 1. **Accessibility:** Low contrast with input placeholders. 53 | 2. **Messy:** There are more than 1 primary buttons. 54 | 3. **Spacing:** The description is too far away from title. Input paddings need some optical corrections. 55 | 4. **Redundant:** The description feels redundant, _"Create an account on Fuma Chat"_ is obviously unnecessary as the user definitely knows it. Also same for the input placeholders. 56 | 57 | #### Optical Correction 58 | 59 | Look at the boxes carefully. 60 | 61 | boxes 62 | 63 | The padding of our first box might looks a bit off, but why? 64 | Human's brain works different from computers. While the xy paddings are actually equal, due to font height (or bounding box), it looks different to us. 65 | 66 | compare 67 | 68 | In fact, it can be caused by many other factors, such as visual weight and even colors. 69 | [This Article about Optical Adjustment](https://marvelapp.com/blog/optical-adjustment-logic-vs-designers) has explained it well. 70 | 71 | ### Correction 72 | 73 | export const inputVariant2 = 74 | "text-neutral-50 bg-neutral-900 rounded-lg w-full px-3 py-2.5 text-xs placeholder:text-neutral-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"; 75 | 76 |
77 |

Create Account

78 |

Try the open-source modern chat app right now.

79 | 80 |
81 | 84 | 85 |
86 |
87 | 90 | 96 |
97 | 98 |
99 | 100 | 101 |
102 |
103 | 104 | Now it looks much better. However, it looks dull. 105 | 106 | To give it a different vibe, let's color it! 107 | 108 | export const inputVariant3 = 109 | "text-neutral-50 bg-slate-400/10 rounded-lg px-3 py-2.5 text-xs placeholder:text-neutral-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-purple-400 focus-visible:bg-slate-400/20"; 110 | 111 |
112 | 113 |

Create Account

114 |

Try the open-source modern chat app right now.

115 | 116 |
117 | 120 | 121 |
122 |
123 | 126 | 132 |
133 | 134 |
135 | 138 | 141 |
142 | 143 |
144 | 155 | 156 | 157 | 158 | 159 | Fuma Chat 160 |
161 |
162 | 163 | Congrats, this is a normal sign-in form you could see in every web app. 164 | UI Design is a wide topic, hope this little article is helpful to you. 165 | -------------------------------------------------------------------------------- /dev/compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | db: 4 | image: postgres:16.4 5 | restart: always 6 | environment: 7 | POSTGRES_USER: user 8 | POSTGRES_DB: comment 9 | POSTGRES_PASSWORD: secret 10 | ports: 11 | - "5432:5432" 12 | volumes: 13 | - pgdata:/var/lib/postgresql/data 14 | volumes: 15 | pgdata: 16 | -------------------------------------------------------------------------------- /lib/cn.ts: -------------------------------------------------------------------------------- 1 | export { twMerge as cn } from "tailwind-merge"; 2 | -------------------------------------------------------------------------------- /lib/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next/types"; 2 | 3 | export const baseUrl = "https://fuma-nama.vercel.app"; 4 | 5 | export function createMetadata(override: Metadata): Metadata { 6 | return { 7 | ...override, 8 | openGraph: { 9 | type: "website", 10 | title: override.title ?? undefined, 11 | description: override.description ?? undefined, 12 | url: baseUrl, 13 | images: [ 14 | { 15 | alt: "banner", 16 | width: "1200", 17 | height: "630", 18 | url: "/banner.png", 19 | }, 20 | ], 21 | siteName: "Portfolio", 22 | ...override.openGraph, 23 | }, 24 | twitter: { 25 | card: "summary_large_image", 26 | title: override.title ?? undefined, 27 | description: override.description ?? undefined, 28 | images: [ 29 | { 30 | alt: "banner", 31 | width: "1200", 32 | height: "630", 33 | url: "/banner.png", 34 | }, 35 | ], 36 | ...override.twitter, 37 | }, 38 | metadataBase: new URL(baseUrl), 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | export const prisma = new PrismaClient(); 4 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { withContent } from "@fuma-content/next"; 3 | import rehypeShiki from "@shikijs/rehype"; 4 | import remarkGfm from "remark-gfm"; 5 | import remarkHeadingId from "remark-heading-id"; 6 | 7 | export default withContent({ 8 | content: { 9 | files: ["./content/**/*"], 10 | outputDir: "./.content", 11 | mdxOptions: { 12 | remarkPlugins: [remarkGfm, [remarkHeadingId, { defaults: true }]], 13 | // @ts-expect-error 14 | rehypePlugins: [[rehypeShiki, { theme: "github-dark" }]], 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fuma-nama", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "db:push": "dotenv -e .env.local -- prisma db push", 11 | "migrate:dev": "dotenv -e .env.local -- prisma migrate dev", 12 | "migrate:deploy": "dotenv -e .env.production -- prisma migrate deploy" 13 | }, 14 | "dependencies": { 15 | "@auth/prisma-adapter": "^2.8.0", 16 | "@fuma-comment/react": "^1.1.0", 17 | "@fuma-comment/server": "^1.1.0", 18 | "@fuma-content/next": "^0.0.2", 19 | "@prisma/client": "^6.4.1", 20 | "@radix-ui/react-scroll-area": "^1.2.3", 21 | "@shikijs/rehype": "^1.11.1", 22 | "class-variance-authority": "^0.7.1", 23 | "feed": "^4.2.2", 24 | "fuma-content": "^0.0.2", 25 | "next": "15.2.1", 26 | "next-auth": "5.0.0-beta.25", 27 | "phenomenon": "^1.6.0", 28 | "react": "^19.0.0", 29 | "react-dom": "^19.0.0", 30 | "react-lite-youtube-embed": "^2.4.0", 31 | "react-spotify-embed": "^2.0.4", 32 | "remark-gfm": "^4.0.1", 33 | "remark-heading-id": "^1.0.1", 34 | "shiki": "^1.11.1", 35 | "tailwind-merge": "^3.0.2" 36 | }, 37 | "devDependencies": { 38 | "@tailwindcss/postcss": "^4.0.12", 39 | "@tailwindcss/typography": "^0.5.16", 40 | "@types/node": "^22.13.10", 41 | "@types/react": "^19.0.10", 42 | "@types/react-dom": "^19.0.4", 43 | "dotenv-cli": "^8.0.0", 44 | "postcss": "^8.5.3", 45 | "prisma": "^6.4.1", 46 | "tailwindcss": "^4.0.12", 47 | "typescript": "^5.8.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /prisma/migrations/0_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Comment" ( 3 | "id" SERIAL NOT NULL, 4 | "page" VARCHAR(256), 5 | "threadId" INTEGER, 6 | "author" VARCHAR(256) NOT NULL, 7 | "content" JSON NOT NULL, 8 | "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | 10 | CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateTable 14 | CREATE TABLE "Rate" ( 15 | "userId" VARCHAR(256) NOT NULL, 16 | "commentId" INTEGER NOT NULL, 17 | "like" BOOLEAN NOT NULL, 18 | 19 | CONSTRAINT "Rate_pkey" PRIMARY KEY ("userId","commentId") 20 | ); 21 | 22 | -- CreateTable 23 | CREATE TABLE "User" ( 24 | "id" TEXT NOT NULL, 25 | "name" TEXT, 26 | "email" TEXT NOT NULL, 27 | "emailVerified" TIMESTAMP(3), 28 | "image" TEXT, 29 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 30 | "updatedAt" TIMESTAMP(3) NOT NULL, 31 | 32 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 33 | ); 34 | 35 | -- CreateTable 36 | CREATE TABLE "Account" ( 37 | "userId" TEXT NOT NULL, 38 | "type" TEXT NOT NULL, 39 | "provider" TEXT NOT NULL, 40 | "providerAccountId" TEXT NOT NULL, 41 | "refresh_token" TEXT, 42 | "access_token" TEXT, 43 | "expires_at" INTEGER, 44 | "token_type" TEXT, 45 | "scope" TEXT, 46 | "id_token" TEXT, 47 | "session_state" TEXT, 48 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 49 | "updatedAt" TIMESTAMP(3) NOT NULL, 50 | 51 | CONSTRAINT "Account_pkey" PRIMARY KEY ("provider","providerAccountId") 52 | ); 53 | 54 | -- CreateTable 55 | CREATE TABLE "Session" ( 56 | "sessionToken" TEXT NOT NULL, 57 | "userId" TEXT NOT NULL, 58 | "expires" TIMESTAMP(3) NOT NULL, 59 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 60 | "updatedAt" TIMESTAMP(3) NOT NULL 61 | ); 62 | 63 | -- CreateTable 64 | CREATE TABLE "VerificationToken" ( 65 | "identifier" TEXT NOT NULL, 66 | "token" TEXT NOT NULL, 67 | "expires" TIMESTAMP(3) NOT NULL, 68 | 69 | CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("identifier","token") 70 | ); 71 | 72 | -- CreateTable 73 | CREATE TABLE "Authenticator" ( 74 | "credentialID" TEXT NOT NULL, 75 | "userId" TEXT NOT NULL, 76 | "providerAccountId" TEXT NOT NULL, 77 | "credentialPublicKey" TEXT NOT NULL, 78 | "counter" INTEGER NOT NULL, 79 | "credentialDeviceType" TEXT NOT NULL, 80 | "credentialBackedUp" BOOLEAN NOT NULL, 81 | "transports" TEXT, 82 | 83 | CONSTRAINT "Authenticator_pkey" PRIMARY KEY ("userId","credentialID") 84 | ); 85 | 86 | -- CreateIndex 87 | CREATE INDEX "Comment_page_idx" ON "Comment"("page"); 88 | 89 | -- CreateIndex 90 | CREATE INDEX "Rate_commentId_idx" ON "Rate"("commentId"); 91 | 92 | -- CreateIndex 93 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 94 | 95 | -- CreateIndex 96 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); 97 | 98 | -- CreateIndex 99 | CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "Authenticator"("credentialID"); 100 | 101 | -- AddForeignKey 102 | ALTER TABLE "Rate" ADD CONSTRAINT "Rate_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE; 103 | 104 | -- AddForeignKey 105 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 106 | 107 | -- AddForeignKey 108 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 109 | 110 | -- AddForeignKey 111 | ALTER TABLE "Authenticator" ADD CONSTRAINT "Authenticator_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 112 | -------------------------------------------------------------------------------- /prisma/migrations/20240724060423_2/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `threadId` on the `Comment` table. All the data in the column will be lost. 5 | - Made the column `page` on table `Comment` required. This step will fail if there are existing NULL values in that column. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Comment" DROP COLUMN "threadId", 10 | ADD COLUMN "thread" INTEGER, 11 | ALTER COLUMN "page" SET NOT NULL; 12 | 13 | -- CreateTable 14 | CREATE TABLE "Role" ( 15 | "userId" VARCHAR(256) NOT NULL, 16 | "name" VARCHAR(256) NOT NULL, 17 | "canDelete" BOOLEAN NOT NULL, 18 | 19 | CONSTRAINT "Role_pkey" PRIMARY KEY ("userId") 20 | ); 21 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | model Role { 2 | userId String @id @db.VarChar(256) 3 | name String @db.VarChar(256) 4 | canDelete Boolean 5 | } 6 | 7 | model Comment { 8 | id Int @id @default(autoincrement()) 9 | page String @db.VarChar(256) 10 | thread Int? 11 | author String @db.VarChar(256) 12 | content Json @db.Json 13 | timestamp DateTime @default(now()) @db.Timestamp() 14 | 15 | rates Rate[] 16 | 17 | @@index([page]) 18 | } 19 | 20 | model Rate { 21 | userId String @db.VarChar(256) 22 | commentId Int 23 | like Boolean 24 | 25 | comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade) 26 | 27 | @@id([userId, commentId]) 28 | @@index([commentId]) 29 | } 30 | 31 | datasource db { 32 | provider = "postgresql" 33 | url = env("DATABASE_URL") 34 | } 35 | 36 | generator client { 37 | provider = "prisma-client-js" 38 | } 39 | 40 | model User { 41 | id String @id @default(cuid()) 42 | name String? 43 | email String @unique 44 | emailVerified DateTime? 45 | image String? 46 | accounts Account[] 47 | sessions Session[] 48 | // Optional for WebAuthn support 49 | Authenticator Authenticator[] 50 | 51 | createdAt DateTime @default(now()) 52 | updatedAt DateTime @updatedAt 53 | } 54 | 55 | model Account { 56 | userId String 57 | type String 58 | provider String 59 | providerAccountId String 60 | refresh_token String? 61 | access_token String? 62 | expires_at Int? 63 | token_type String? 64 | scope String? 65 | id_token String? 66 | session_state String? 67 | 68 | createdAt DateTime @default(now()) 69 | updatedAt DateTime @updatedAt 70 | 71 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 72 | 73 | @@id([provider, providerAccountId]) 74 | } 75 | 76 | model Session { 77 | sessionToken String @unique 78 | userId String 79 | expires DateTime 80 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 81 | 82 | createdAt DateTime @default(now()) 83 | updatedAt DateTime @updatedAt 84 | } 85 | 86 | model VerificationToken { 87 | identifier String 88 | token String 89 | expires DateTime 90 | 91 | @@id([identifier, token]) 92 | } 93 | 94 | // Optional for WebAuthn support 95 | model Authenticator { 96 | credentialID String @unique 97 | userId String 98 | providerAccountId String 99 | credentialPublicKey String 100 | counter Int 101 | credentialDeviceType String 102 | credentialBackedUp Boolean 103 | transports String? 104 | 105 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 106 | 107 | @@id([userId, credentialID]) 108 | } -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/public/banner.png -------------------------------------------------------------------------------- /public/blog/clerk-toc-highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/public/blog/clerk-toc-highlight.png -------------------------------------------------------------------------------- /public/blog/fumadocs-toc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/public/blog/fumadocs-toc.png -------------------------------------------------------------------------------- /public/blog/toc-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/public/blog/toc-2.png -------------------------------------------------------------------------------- /public/blog/toc-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/public/blog/toc-3.png -------------------------------------------------------------------------------- /public/blog/toc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/public/blog/toc.png -------------------------------------------------------------------------------- /public/eve.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/public/eve.jpg -------------------------------------------------------------------------------- /public/iori-kanzaki.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/public/iori-kanzaki.jpg -------------------------------------------------------------------------------- /public/markup-studio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/public/markup-studio.png -------------------------------------------------------------------------------- /public/me.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/public/me.jpg -------------------------------------------------------------------------------- /public/ui-before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/public/ui-before.png -------------------------------------------------------------------------------- /public/ui-compare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/public/ui-compare.png -------------------------------------------------------------------------------- /public/yorushika.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuma-nama/fuma/5ea588f3076ae4a4545223ff90f4e1bced696f7b/public/yorushika.jpg -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": [ 26 | "./*" 27 | ], 28 | "content": [ 29 | "./.content/index.js" 30 | ] 31 | }, 32 | "target": "ES2017" 33 | }, 34 | "include": [ 35 | "next-env.d.ts", 36 | "**/*.ts", 37 | "**/*.tsx", 38 | ".next/types/**/*.ts" 39 | ], 40 | "exclude": [ 41 | "node_modules" 42 | ] 43 | } 44 | --------------------------------------------------------------------------------