├── .husky └── pre-commit ├── .prettierignore ├── app ├── favicon.ico ├── share │ └── [id] │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── loading.tsx ├── robots.ts ├── sitemap.ts ├── auth │ ├── login │ │ └── page.tsx │ ├── sign-up │ │ └── page.tsx │ ├── forgot-password │ │ └── page.tsx │ ├── update-password │ │ └── page.tsx │ ├── confirm │ │ └── route.ts │ ├── sign-up-success │ │ └── page.tsx │ ├── callback │ │ └── route.ts │ └── error │ │ └── page.tsx ├── actions │ ├── delete-graph-action.ts │ ├── save-graph.ts │ ├── request-openlib-books.ts │ └── generate-graph.ts ├── layout.tsx ├── create │ ├── loading.tsx │ └── page.tsx ├── loading.tsx ├── dashboard │ ├── loading.tsx │ └── page.tsx ├── profile │ ├── [username] │ │ ├── loading.tsx │ │ └── page.tsx │ └── page.tsx ├── globals.css └── page.tsx ├── consts ├── github-repo-url.ts ├── open-library-search-url.ts ├── system-instruction.ts └── landing-page-content.ts ├── postcss.config.mjs ├── .prettierrc ├── types ├── delete-graph-dialog-props.ts ├── book.ts ├── generate-graph-response.ts ├── open-library-doc.ts ├── open-library-response.ts ├── save-graph-params.ts ├── edit-graph-dialog-props.ts └── mermaid-graph-props.ts ├── public └── jane-austen-inspired-illustrations_logo.png ├── utils ├── highlighter.ts ├── cn.ts ├── get-graph-file-name.ts ├── copy-mermaid-syntax.ts ├── copy-url.ts ├── download-svg.ts └── download-png.ts ├── next.config.ts ├── lib └── supabase │ ├── client.ts │ ├── server.ts │ └── middleware.ts ├── .gitignore ├── middleware.ts ├── components ├── LogoutButton.tsx ├── ui │ ├── sonner.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── avatar.tsx │ ├── input.tsx │ ├── switch.tsx │ ├── card.tsx │ ├── button.tsx │ ├── dropdown-menu.tsx │ └── dialog.tsx ├── UpdatePasswordForm.tsx ├── DeleteGraphDialog.tsx ├── EditGraphDialog.tsx ├── ForgotPasswordForm.tsx ├── LoginForm.tsx ├── SignUpForm.tsx ├── TopBar.tsx └── GraphCard.tsx ├── components.json ├── tsconfig.json ├── eslint.config.mjs ├── LICENSE ├── package.json └── README.md /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herol3oy/austen/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /consts/github-repo-url.ts: -------------------------------------------------------------------------------- 1 | export const GITHUB_REPO_URL = 'https://api.github.com/repos/herol3oy/austen' 2 | -------------------------------------------------------------------------------- /consts/open-library-search-url.ts: -------------------------------------------------------------------------------- 1 | export const OPEN_LIBRARY_SEARCH_URL = 'https://openlibrary.org/search.json' 2 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ['@tailwindcss/postcss'], 3 | } 4 | 5 | export default config 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "plugins": ["prettier-plugin-tailwindcss"] 5 | } 6 | -------------------------------------------------------------------------------- /types/delete-graph-dialog-props.ts: -------------------------------------------------------------------------------- 1 | export interface DeleteGraphDialogProps { 2 | graphId: string 3 | graphTitle: string 4 | } 5 | -------------------------------------------------------------------------------- /types/book.ts: -------------------------------------------------------------------------------- 1 | export interface Book { 2 | key: string 3 | title: string 4 | author_name: string 5 | coverImageUrl?: string 6 | } 7 | -------------------------------------------------------------------------------- /public/jane-austen-inspired-illustrations_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herol3oy/austen/HEAD/public/jane-austen-inspired-illustrations_logo.png -------------------------------------------------------------------------------- /types/generate-graph-response.ts: -------------------------------------------------------------------------------- 1 | export interface GenerateGraphResponse { 2 | mermaidSyntax: string 3 | emojis: string 4 | error?: string 5 | } 6 | -------------------------------------------------------------------------------- /types/open-library-doc.ts: -------------------------------------------------------------------------------- 1 | export interface OpenLibraryDoc { 2 | key: string 3 | title: string 4 | author_name: string[] 5 | cover_i?: number 6 | } 7 | -------------------------------------------------------------------------------- /types/open-library-response.ts: -------------------------------------------------------------------------------- 1 | import { OpenLibraryDoc } from './open-library-doc' 2 | 3 | export interface OpenLibraryResponse { 4 | docs: OpenLibraryDoc[] 5 | } 6 | -------------------------------------------------------------------------------- /types/save-graph-params.ts: -------------------------------------------------------------------------------- 1 | export interface SaveGraphParams { 2 | bookName: string 3 | authorName: string 4 | svgGraph: string 5 | mermaidSyntax: string 6 | emojis: string 7 | } 8 | -------------------------------------------------------------------------------- /utils/highlighter.ts: -------------------------------------------------------------------------------- 1 | import { createHighlighter } from 'shiki' 2 | 3 | export const highlighter = await createHighlighter({ 4 | themes: ['light-plus'], 5 | langs: ['mermaid'], 6 | }) 7 | -------------------------------------------------------------------------------- /app/share/[id]/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function ShareLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | return
{children}
7 | } 8 | -------------------------------------------------------------------------------- /utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export const cn = (...inputs: ClassValue[]) => { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /types/edit-graph-dialog-props.ts: -------------------------------------------------------------------------------- 1 | export interface EditGraphDialogProps { 2 | isOpen: boolean 3 | onClose: () => void 4 | onSave: (syntax: string) => Promise 5 | initialSyntax: string 6 | bookTitle: string 7 | } 8 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next' 2 | 3 | const nextConfig: NextConfig = { 4 | images: { 5 | remotePatterns: [new URL('https://covers.openlibrary.org/**')], 6 | }, 7 | } 8 | 9 | export default nextConfig 10 | -------------------------------------------------------------------------------- /utils/get-graph-file-name.ts: -------------------------------------------------------------------------------- 1 | export const generateGraphFileName = ( 2 | title = 'untitled', 3 | author = 'unknown', 4 | ) => { 5 | return `austen-pages.dev-${title.toLowerCase().replace(/\s+/g, '-')}-${author.toLowerCase().replace(/\s+/g, '-')}-graph` 6 | } 7 | -------------------------------------------------------------------------------- /lib/supabase/client.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserClient } from '@supabase/ssr' 2 | 3 | export const createClient = () => { 4 | return createBrowserClient( 5 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 6 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /types/mermaid-graph-props.ts: -------------------------------------------------------------------------------- 1 | export interface MermaidGraphProps { 2 | graphDefinition: string 3 | emojis: string 4 | title: string 5 | author: string 6 | graphId?: string 7 | isPublic?: boolean 8 | isShared?: boolean 9 | userId?: string 10 | coverImageUrl?: string 11 | } 12 | -------------------------------------------------------------------------------- /app/robots.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from 'next' 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | return { 5 | rules: { 6 | userAgent: '*', 7 | allow: '/', 8 | disallow: '/dashboard/', 9 | }, 10 | sitemap: 'https://acme.com/sitemap.xml', 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from 'next' 2 | 3 | export default function sitemap(): MetadataRoute.Sitemap { 4 | return [ 5 | { 6 | url: 'https://austen.vercel.app', 7 | lastModified: new Date(), 8 | changeFrequency: 'monthly', 9 | priority: 1, 10 | }, 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /app/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from '@/components/LoginForm' 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /app/auth/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUpForm } from '@/components/SignUpForm' 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /app/auth/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { ForgotPasswordForm } from '@/components/ForgotPasswordForm' 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /app/auth/update-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { UpdatePasswordForm } from '@/components/UpdatePasswordForm' 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.pnp 3 | .pnp.* 4 | .yarn/* 5 | !.yarn/patches 6 | !.yarn/plugins 7 | !.yarn/releases 8 | !.yarn/versions 9 | 10 | /coverage 11 | 12 | /.next/ 13 | /out/ 14 | 15 | /build 16 | 17 | .DS_Store 18 | *.pem 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | .pnpm-debug.log* 24 | 25 | .env* 26 | 27 | .vercel 28 | 29 | *.tsbuildinfo 30 | next-env.d.ts 31 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest } from 'next/server' 2 | 3 | import { updateSession } from '@/lib/supabase/middleware' 4 | 5 | export const middleware = async (request: NextRequest) => { 6 | return await updateSession(request) 7 | } 8 | 9 | export const config = { 10 | matcher: [ 11 | '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /utils/copy-mermaid-syntax.ts: -------------------------------------------------------------------------------- 1 | export const copyMermaidToClipboard = async ( 2 | graphDefinition: string, 3 | setIsCopied: (value: boolean) => void, 4 | ) => { 5 | try { 6 | await navigator.clipboard.writeText(graphDefinition) 7 | setIsCopied(true) 8 | setTimeout(() => setIsCopied(false), 2000) 9 | } catch (error) { 10 | console.error('Failed to copy syntax:', error) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /utils/copy-url.ts: -------------------------------------------------------------------------------- 1 | export const copyUrlToClipboard = async ( 2 | urlInputRef: React.RefObject, 3 | setUrlCopied: (value: boolean) => void, 4 | ) => { 5 | if (urlInputRef.current) { 6 | urlInputRef.current.select() 7 | await navigator.clipboard.writeText(urlInputRef.current.value) 8 | setUrlCopied(true) 9 | setTimeout(() => setUrlCopied(false), 2000) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /components/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useRouter } from 'next/navigation' 4 | 5 | import { Button } from '@/components/ui/button' 6 | import { createClient } from '@/lib/supabase/client' 7 | 8 | export function LogoutButton() { 9 | const router = useRouter() 10 | 11 | const logout = async () => { 12 | const supabase = createClient() 13 | await supabase.auth.signOut() 14 | router.push('/auth/login') 15 | } 16 | 17 | return 18 | } 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /utils/download-svg.ts: -------------------------------------------------------------------------------- 1 | import { generateGraphFileName } from '@/utils/get-graph-file-name' 2 | 3 | export const exportGraphAsSvg = ( 4 | svgContent: string, 5 | title: string, 6 | author: string, 7 | ) => { 8 | if (!svgContent) return 9 | 10 | const blob = new Blob([svgContent], { type: 'image/svg+xml' }) 11 | const url = window.URL.createObjectURL(blob) 12 | const link = document.createElement('a') 13 | link.href = url 14 | link.download = `${generateGraphFileName(title, author)}.svg` 15 | document.body.appendChild(link) 16 | link.click() 17 | document.body.removeChild(link) 18 | window.URL.revokeObjectURL(url) 19 | } 20 | -------------------------------------------------------------------------------- /components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useTheme } from 'next-themes' 4 | import { Toaster as Sonner, ToasterProps } from 'sonner' 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = 'system' } = useTheme() 8 | 9 | return ( 10 | 22 | ) 23 | } 24 | 25 | export { Toaster } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as LabelPrimitive from '@radix-ui/react-label' 4 | import * as React from 'react' 5 | 6 | import { cn } from '@/utils/cn' 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /lib/supabase/server.ts: -------------------------------------------------------------------------------- 1 | import { createServerClient } from '@supabase/ssr' 2 | import { cookies } from 'next/headers' 3 | 4 | export const createClient = async () => { 5 | const cookieStore = await cookies() 6 | 7 | return createServerClient( 8 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 9 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 10 | { 11 | cookies: { 12 | getAll() { 13 | return cookieStore.getAll() 14 | }, 15 | setAll(cookiesToSet) { 16 | try { 17 | cookiesToSet.forEach(({ name, value, options }) => 18 | cookieStore.set(name, value, options), 19 | ) 20 | } catch {} 21 | }, 22 | }, 23 | }, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /utils/download-png.ts: -------------------------------------------------------------------------------- 1 | import domtoimage from 'dom-to-image' 2 | import type { RefObject } from 'react' 3 | 4 | import { generateGraphFileName } from '@/utils/get-graph-file-name' 5 | 6 | export const exportGraphAsPng = async ( 7 | graphRef: RefObject, 8 | title: string, 9 | author: string, 10 | ) => { 11 | if (!graphRef.current) return 12 | 13 | try { 14 | const dataUrl = await domtoimage.toPng(graphRef.current) 15 | const link = document.createElement('a') 16 | link.href = dataUrl 17 | link.download = `${generateGraphFileName(title, author)}.png` 18 | document.body.appendChild(link) 19 | link.click() 20 | document.body.removeChild(link) 21 | } catch (error) { 22 | console.error('Error generating PNG:', error) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from '@eslint/eslintrc' 2 | import simpleImportSort from 'eslint-plugin-simple-import-sort' 3 | import { dirname } from 'path' 4 | import { fileURLToPath } from 'url' 5 | 6 | const __filename = fileURLToPath(import.meta.url) 7 | const __dirname = dirname(__filename) 8 | 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | }) 12 | 13 | const eslintConfig = [ 14 | ...compat.extends('next/core-web-vitals', 'next/typescript'), 15 | { 16 | ignores: ['.next/**/*'], 17 | }, 18 | { 19 | plugins: { 20 | 'simple-import-sort': simpleImportSort, 21 | }, 22 | rules: { 23 | 'simple-import-sort/imports': 'error', 24 | 'simple-import-sort/exports': 'error', 25 | }, 26 | }, 27 | ] 28 | 29 | export default eslintConfig 30 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/utils/cn' 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { 6 | return ( 7 |