├── .gitignore ├── .learning ├── fasthtml │ ├── .sesskey │ ├── imagegen.py │ ├── logger.py │ ├── main.py │ ├── todo.py │ ├── todos.db │ ├── todos.db-shm │ └── todos.db-wal ├── nextjs │ └── nextjs-dashboard │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app │ │ ├── actions │ │ │ └── auth.ts │ │ ├── dashboard │ │ │ ├── (overview) │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── customers │ │ │ │ └── page.tsx │ │ │ ├── invoices │ │ │ │ ├── [id] │ │ │ │ │ └── edit │ │ │ │ │ │ ├── not-found.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── create │ │ │ │ │ └── page.tsx │ │ │ │ ├── error.tsx │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── favicon.ico │ │ ├── layout.tsx │ │ ├── login │ │ │ └── page.tsx │ │ ├── opengraph-image.png │ │ ├── page.tsx │ │ ├── seed │ │ │ └── route.ts │ │ └── ui │ │ │ ├── acme-logo.tsx │ │ │ ├── button.tsx │ │ │ ├── customers │ │ │ └── table.tsx │ │ │ ├── dashboard │ │ │ ├── cards.tsx │ │ │ ├── latest-invoices.tsx │ │ │ ├── nav-links.tsx │ │ │ ├── revenue-chart.tsx │ │ │ └── sidenav.tsx │ │ │ ├── fonts.ts │ │ │ ├── global.css │ │ │ ├── home.module.css │ │ │ ├── invoices │ │ │ ├── breadcrumbs.tsx │ │ │ ├── buttons.tsx │ │ │ ├── create-form.tsx │ │ │ ├── edit-form.tsx │ │ │ ├── pagination.tsx │ │ │ ├── status.tsx │ │ │ └── table.tsx │ │ │ ├── login-form.tsx │ │ │ ├── search.tsx │ │ │ └── skeletons.tsx │ │ ├── auth.config.ts │ │ ├── auth.ts │ │ ├── middleware.ts │ │ ├── next.config.mjs │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── postcss.config.js │ │ ├── public │ │ ├── customers │ │ │ ├── amy-burns.png │ │ │ ├── balazs-orban.png │ │ │ ├── delba-de-oliveira.png │ │ │ ├── evil-rabbit.png │ │ │ ├── lee-robinson.png │ │ │ └── michael-novotny.png │ │ ├── hero-desktop.png │ │ └── hero-mobile.png │ │ ├── tailwind.config.ts │ │ └── tsconfig.json └── react │ ├── app │ ├── layout.js │ ├── like-button.jsx │ └── page.jsx │ ├── package-lock.json │ └── package.json ├── LICENSE ├── README.md ├── fastapi+svelte ├── database.sqlite ├── logger.py ├── main.py └── svelte-app │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ ├── app.d.ts │ ├── app.html │ └── routes │ │ └── +page.svelte │ ├── svelte.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── fastapi ├── logger.py ├── main.py ├── static │ ├── script.js │ └── style.css └── templates │ └── index.html ├── fasthtml ├── logger.py ├── main-jeremy.py ├── main.py └── static │ └── style.css ├── nextjs ├── .eslintrc.json ├── README.md ├── app │ ├── api │ │ ├── data │ │ │ └── route.ts │ │ ├── download │ │ │ └── route.ts │ │ └── upload │ │ │ └── route.ts │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── DataTable.tsx │ ├── DownloadButton.tsx │ └── FileUpload.tsx ├── lib │ └── db.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── tailwind.config.ts └── tsconfig.json ├── pyproject.toml ├── sample-data.csv ├── svelte ├── .npmrc ├── .prettierignore ├── .prettierrc ├── README.md ├── data.sqlite ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── src │ ├── app.d.ts │ ├── app.html │ ├── hooks.server.ts │ ├── lib │ │ ├── api.ts │ │ ├── components │ │ │ ├── CsvUpload.svelte │ │ │ └── Table.svelte │ │ └── db.ts │ └── routes │ │ ├── +page.svelte │ │ └── api │ │ ├── data │ │ ├── +server.ts │ │ └── [id] │ │ │ └── +server.ts │ │ ├── download │ │ └── +server.ts │ │ └── upload │ │ └── +server.ts ├── svelte.config.js ├── tsconfig.json └── vite.config.ts └── uv.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | # JS 165 | **/node_modules 166 | **/.next 167 | **/next-env.d.ts 168 | **/.svelte-kit 169 | 170 | # Other files 171 | .DS_Store 172 | .vscode 173 | database.db -------------------------------------------------------------------------------- /.learning/fasthtml/.sesskey: -------------------------------------------------------------------------------- 1 | ed779431-f5ee-4604-a7cf-780d20ee9855 -------------------------------------------------------------------------------- /.learning/fasthtml/imagegen.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import replicate 4 | import requests 5 | from fastcore.parallel import threaded 6 | from fasthtml.common import * 7 | from PIL import Image 8 | 9 | app = FastHTML(hdrs=(picolink,)) 10 | 11 | # Replicate setup (for image generation) 12 | replicate_api_token = os.environ["REPLICATE_API_KEY"] 13 | client = replicate.Client(api_token=replicate_api_token) 14 | 15 | # Store our generations 16 | generations = [] 17 | folder = "gens/" 18 | os.makedirs(folder, exist_ok=True) 19 | 20 | 21 | # Main page 22 | @app.get("/") 23 | def home(): 24 | inp = Input(id="new-prompt", name="prompt", placeholder="Enter a prompt") 25 | add = Form( 26 | Group(inp, Button("Generate")), 27 | hx_post="/", 28 | target_id="gen-list", 29 | hx_swap="afterbegin", 30 | ) 31 | gen_list = Div(id="gen-list") 32 | return Title("Image Generation Demo"), Main( 33 | H1("Magic Image Generation"), add, gen_list, cls="container" 34 | ) 35 | 36 | 37 | # A pending preview keeps polling this route until we return the image preview 38 | def generation_preview(id): 39 | if os.path.exists(f"gens/{id}.png"): 40 | return Div(Img(src=f"/gens/{id}.png"), id=f"gen-{id}") 41 | else: 42 | return Div( 43 | "Generating...", 44 | id=f"gen-{id}", 45 | hx_post=f"/generations/{id}", 46 | hx_trigger="every 1s", 47 | hx_swap="outerHTML", 48 | ) 49 | 50 | 51 | @app.post("/generations/{id}") 52 | def get(id: int): 53 | return generation_preview(id) 54 | 55 | 56 | # For images, CSS, etc. 57 | @app.get("/{fname:path}.{ext:static}") 58 | def static(fname: str, ext: str): 59 | return FileResponse(f"{fname}.{ext}") 60 | 61 | 62 | # Generation route 63 | @app.post("/") 64 | def post(prompt: str): 65 | id = len(generations) 66 | generate_and_save(prompt, id) 67 | generations.append(prompt) 68 | clear_input = Input( 69 | id="new-prompt", name="prompt", placeholder="Enter a prompt", hx_swap_oob="true" 70 | ) 71 | return generation_preview(id), clear_input 72 | 73 | 74 | # Generate an image and save it to the folder (in a separate thread) 75 | @threaded 76 | def generate_and_save(prompt, id): 77 | output = client.run( 78 | "playgroundai/playground-v2.5-1024px-aesthetic:a45f82a1382bed5c7aeb861dac7c7d191b0fdf74d8d57c4a0e6ed7d4d0bf7d24", 79 | input={ 80 | "width": 1024, 81 | "height": 1024, 82 | "prompt": prompt, 83 | "scheduler": "DPMSolver++", 84 | "num_outputs": 1, 85 | "guidance_scale": 3, 86 | "apply_watermark": True, 87 | "negative_prompt": "ugly, deformed, noisy, blurry, distorted", 88 | "prompt_strength": 0.8, 89 | "num_inference_steps": 25, 90 | }, 91 | ) 92 | Image.open(requests.get(output[0], stream=True).raw).save(f"{folder}/{id}.png") 93 | return True 94 | 95 | 96 | serve() 97 | -------------------------------------------------------------------------------- /.learning/fasthtml/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | 5 | def get_logger(name: str = __name__, level: str = "INFO") -> logging.Logger: 6 | """ 7 | Initialize a simple logger that outputs to the console. 8 | 9 | Args: 10 | name (str): The name of the logger. Defaults to the module name. 11 | level (str): The logging level. Defaults to "INFO". 12 | 13 | Returns: 14 | logging.Logger: A configured logger instance. 15 | """ 16 | # Create a logger 17 | logger = logging.getLogger(name) 18 | 19 | # Set the logging level 20 | level = getattr(logging, level.upper(), logging.INFO) 21 | logger.setLevel(level) 22 | 23 | # Create a console handler and set its level 24 | console_handler = logging.StreamHandler(sys.stdout) 25 | console_handler.setLevel(level) 26 | 27 | # Create a formatter 28 | formatter = logging.Formatter( 29 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 30 | ) 31 | 32 | # Add the formatter to the console handler 33 | console_handler.setFormatter(formatter) 34 | 35 | # Add the console handler to the logger 36 | logger.addHandler(console_handler) 37 | 38 | return logger 39 | 40 | 41 | # Example usage 42 | if __name__ == "__main__": 43 | logger = get_logger("example_logger", "DEBUG") 44 | logger.debug("This is a debug message") 45 | logger.info("This is an info message") 46 | logger.warning("This is a warning message") 47 | logger.error("This is an error message") 48 | logger.critical("This is a critical message") 49 | -------------------------------------------------------------------------------- /.learning/fasthtml/main.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | from logger import get_logger 3 | 4 | # Import logger 5 | logger = get_logger("fastapi", "INFO") 6 | 7 | # Set up fastHTML app 8 | css = Style(":root {--pico-font-size:90%,--pico-font-family: Pacifico, cursive;}") 9 | app = FastHTMLWithLiveReload(hdrs=(picolink, css)) 10 | 11 | messages = ["This is a message, which will get rendered as a paragraph"] 12 | 13 | 14 | count = 0 15 | 16 | 17 | @app.get("/") 18 | def home(): 19 | return Title("Count Demo"), Main( 20 | H1("Count Demo"), 21 | P(f"Count is set to {count}", id="count"), 22 | Button( 23 | "Increment", hx_post="/increment", hx_target="#count", hx_swap="innerHTML" 24 | ), 25 | ) 26 | 27 | 28 | @app.post("/increment") 29 | def increment(): 30 | print("incrementing") 31 | global count 32 | count += 1 33 | return f"Count is set to {count}" 34 | 35 | 36 | serve() 37 | -------------------------------------------------------------------------------- /.learning/fasthtml/todo.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | 3 | 4 | def render_todo(todo): 5 | tid = f"todo-{todo.id}" 6 | toggle = A("Toggle", hx_get=f"/toggle/{todo.id}", target_id=tid) 7 | delete = A("Delete", hx_delete=f"/{todo.id}", hx_swap="outerHTML", target_id=tid) 8 | return Li(toggle, delete, todo.title + (" ✅" if todo.done else ""), id=tid) 9 | 10 | 11 | app, rt, todos, Todo = fast_app( 12 | "todos.db", live=True, render=render_todo, id=int, pk="id", title=str, done=bool 13 | ) # Returns a FastHTML app, a router, a todos table, and a Todos class 14 | 15 | 16 | def make_input(): 17 | return Input(placeholder="Add a todo", id="title", hx_swap_oob="true") 18 | 19 | 20 | @rt("/") 21 | def get(): 22 | frm = Form( 23 | Group(make_input(), Button("Add")), 24 | hx_post="/", 25 | target_id="todo-list", 26 | hx_swap="beforeend", 27 | ) 28 | return Titled("Todos", Card(Ul(*todos(), id="todo-list"), header=frm)) 29 | 30 | 31 | @rt("/{tid}") 32 | def delete(tid: int): 33 | todos.delete(tid) 34 | 35 | 36 | @rt("/") 37 | def post(todo: Todo): 38 | return todos.insert(todo), make_input() 39 | 40 | 41 | @rt("/toggle/{tid}") 42 | def get(tid: int): 43 | todo = todos.get(tid) 44 | todo.done = not todo.done 45 | return todos.update(todo) 46 | 47 | 48 | serve() 49 | -------------------------------------------------------------------------------- /.learning/fasthtml/todos.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/.learning/fasthtml/todos.db -------------------------------------------------------------------------------- /.learning/fasthtml/todos.db-shm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/.learning/fasthtml/todos.db-shm -------------------------------------------------------------------------------- /.learning/fasthtml/todos.db-wal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/.learning/fasthtml/todos.db-wal -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals" 4 | ], 5 | "parser": "@typescript-eslint/parser", 6 | "plugins": ["@typescript-eslint"], 7 | "rules": {} 8 | } 9 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/README.md: -------------------------------------------------------------------------------- 1 | ## Next.js App Router Course - Starter 2 | 3 | This is the starter template for the Next.js App Router Course. It contains the starting code for the dashboard application. 4 | 5 | For more information, see the [course curriculum](https://nextjs.org/learn) on the Next.js Website. 6 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/actions/auth.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { signOut } from '@/auth'; 4 | 5 | export async function handleSignOut() { 6 | await signOut(); 7 | } 8 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/dashboard/(overview)/loading.tsx: -------------------------------------------------------------------------------- 1 | import DashboardSkeleton from '@/app/ui/skeletons'; 2 | 3 | export default function Loading() { 4 | return ; 5 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/dashboard/(overview)/page.tsx: -------------------------------------------------------------------------------- 1 | import CardWrapper from '@/app/ui/dashboard/cards'; 2 | import RevenueChart from '@/app/ui/dashboard/revenue-chart'; 3 | import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; 4 | import { lusitana } from '@/app/ui/fonts'; 5 | import { Suspense } from 'react'; 6 | import { RevenueChartSkeleton, LatestInvoicesSkeleton, CardsSkeleton } from '@/app/ui/skeletons'; 7 | 8 | export default async function Page() { 9 | 10 | return ( 11 |
12 |

13 | Dashboard 14 |

15 |
16 | }> 17 | 18 | 19 |
20 |
21 | }> 22 | 23 | 24 | }> 25 | 26 | 27 |
28 |
29 | ); 30 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/dashboard/customers/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'Customers', 5 | }; 6 | 7 | export default function Page() { 8 | return

Customers Page

; 9 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/dashboard/invoices/[id]/edit/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { FaceFrownIcon } from '@heroicons/react/24/outline'; 3 | 4 | export default function NotFound() { 5 | return ( 6 |
7 | 8 |

404 Not Found

9 |

Could not find the requested invoice.

10 | 14 | Go Back 15 | 16 |
17 | ); 18 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/dashboard/invoices/[id]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import Form from '@/app/ui/invoices/edit-form'; 2 | import Breadcrumbs from '@/app/ui/invoices/breadcrumbs'; 3 | import { fetchCustomers, fetchInvoiceById } from '@/app/lib/data'; 4 | import { notFound } from 'next/navigation'; 5 | 6 | export default async function Page( {params}: {params: {id: string}}) { 7 | const id = params.id; 8 | const [invoice, customers] = await Promise.all([ 9 | fetchInvoiceById(id), 10 | fetchCustomers(), 11 | ]); 12 | 13 | if (!invoice) { 14 | notFound(); 15 | } 16 | 17 | return ( 18 |
19 | 29 |
30 |
31 | ); 32 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/dashboard/invoices/create/page.tsx: -------------------------------------------------------------------------------- 1 | import Form from '@/app/ui/invoices/create-form'; 2 | import Breadcrumbs from '@/app/ui/invoices/breadcrumbs'; 3 | import { fetchCustomers } from '@/app/lib/data'; 4 | 5 | export default async function Page() { 6 | const customers = await fetchCustomers(); 7 | 8 | return ( 9 |
10 | 20 | 21 |
22 | ); 23 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/dashboard/invoices/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect } from 'react'; 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error & { digest?: string }; 10 | reset: () => void; 11 | }) { 12 | useEffect(() => { 13 | // Optionally log the error to an error reporting service 14 | console.error(error); 15 | }, [error]); 16 | 17 | return ( 18 |
19 |

Something went wrong!

20 | 29 |
30 | ); 31 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/dashboard/invoices/page.tsx: -------------------------------------------------------------------------------- 1 | import Pagination from '@/app/ui/invoices/pagination'; 2 | import Search from '@/app/ui/search'; 3 | import Table from '@/app/ui/invoices/table'; 4 | import { CreateInvoice } from '@/app/ui/invoices/buttons'; 5 | import { lusitana } from '@/app/ui/fonts'; 6 | import { InvoicesTableSkeleton } from '@/app/ui/skeletons'; 7 | import { Suspense } from 'react'; 8 | import { fetchInvoicesPages } from '@/app/lib/data'; 9 | import { Metadata } from 'next'; 10 | 11 | export const metadata: Metadata = { 12 | title: 'Invoices', 13 | }; 14 | 15 | export default async function Page({ 16 | searchParams, 17 | }: { 18 | searchParams?: { 19 | query?: string; 20 | page?: string; 21 | }; 22 | }) { 23 | const query = searchParams?.query || ''; 24 | const currentPage = Number(searchParams?.page) || 1; 25 | const totalPages = await fetchInvoicesPages(query); 26 | return ( 27 |
28 |
29 |

Invoices

30 |
31 |
32 | 33 | 34 |
35 | }> 36 | 37 | 38 |
39 | 40 |
41 | 42 | ); 43 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import SideNav from '@/app/ui/dashboard/sidenav'; 4 | 5 | export const experimental_ppr = true; 6 | 7 | export default function Layout({ children }: { children: React.ReactNode }) { 8 | return ( 9 |
10 |
11 | 12 |
13 |
{children}
14 |
15 | ); 16 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/.learning/nextjs/nextjs-dashboard/app/favicon.ico -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import '@/app/ui/global.css'; 2 | import { inter } from '@/app/ui/fonts'; 3 | import { Metadata } from 'next'; 4 | 5 | export const metadata: Metadata = { 6 | title: { 7 | template: '%s | Acme Dashboard', 8 | default: 'Acme Dashboard', 9 | }, 10 | description: 'The official Next.js Learn Dashboard built with App Router.', 11 | metadataBase: new URL('https://next-learn-dashboard.vercel.sh'), 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }) { 19 | return ( 20 | 21 | {children} 22 | 23 | ); 24 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import AcmeLogo from '@/app/ui/acme-logo'; 2 | import LoginForm from '@/app/ui/login-form'; 3 | import { Metadata } from 'next'; 4 | 5 | export const metadata: Metadata = { 6 | title: 'Login', 7 | }; 8 | 9 | export default function LoginPage() { 10 | return ( 11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | ); 22 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/.learning/nextjs/nextjs-dashboard/app/opengraph-image.png -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/page.tsx: -------------------------------------------------------------------------------- 1 | import AcmeLogo from '@/app/ui/acme-logo'; 2 | import { ArrowRightIcon } from '@heroicons/react/24/outline'; 3 | import Link from 'next/link'; 4 | import { lusitana } from '@/app/ui/fonts'; 5 | import Image from 'next/image'; 6 | 7 | 8 | export default function Page() { 9 | return ( 10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 |

18 | Welcome to Acme. This is the example for the{' '} 19 | 20 | Next.js Learn Course 21 | 22 | , brought to you by Vercel. 23 |

24 | 28 | Log in 29 | 30 |
31 |
32 | {/* Add Hero Images Here */} 33 | 40 | 47 |
48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/seed/route.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import { db } from '@vercel/postgres'; 3 | import { invoices, customers, revenue, users } from '../lib/placeholder-data'; 4 | 5 | const client = await db.connect(); 6 | 7 | async function seedUsers() { 8 | await client.sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`; 9 | await client.sql` 10 | CREATE TABLE IF NOT EXISTS users ( 11 | id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, 12 | name VARCHAR(255) NOT NULL, 13 | email TEXT NOT NULL UNIQUE, 14 | password TEXT NOT NULL 15 | ); 16 | `; 17 | 18 | const insertedUsers = await Promise.all( 19 | users.map(async (user) => { 20 | const hashedPassword = await bcrypt.hash(user.password, 10); 21 | return client.sql` 22 | INSERT INTO users (id, name, email, password) 23 | VALUES (${user.id}, ${user.name}, ${user.email}, ${hashedPassword}) 24 | ON CONFLICT (id) DO NOTHING; 25 | `; 26 | }), 27 | ); 28 | 29 | return insertedUsers; 30 | } 31 | 32 | async function seedInvoices() { 33 | await client.sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`; 34 | 35 | await client.sql` 36 | CREATE TABLE IF NOT EXISTS invoices ( 37 | id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, 38 | customer_id UUID NOT NULL, 39 | amount INT NOT NULL, 40 | status VARCHAR(255) NOT NULL, 41 | date DATE NOT NULL 42 | ); 43 | `; 44 | 45 | const insertedInvoices = await Promise.all( 46 | invoices.map( 47 | (invoice) => client.sql` 48 | INSERT INTO invoices (customer_id, amount, status, date) 49 | VALUES (${invoice.customer_id}, ${invoice.amount}, ${invoice.status}, ${invoice.date}) 50 | ON CONFLICT (id) DO NOTHING; 51 | `, 52 | ), 53 | ); 54 | 55 | return insertedInvoices; 56 | } 57 | 58 | async function seedCustomers() { 59 | await client.sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`; 60 | 61 | await client.sql` 62 | CREATE TABLE IF NOT EXISTS customers ( 63 | id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, 64 | name VARCHAR(255) NOT NULL, 65 | email VARCHAR(255) NOT NULL, 66 | image_url VARCHAR(255) NOT NULL 67 | ); 68 | `; 69 | 70 | const insertedCustomers = await Promise.all( 71 | customers.map( 72 | (customer) => client.sql` 73 | INSERT INTO customers (id, name, email, image_url) 74 | VALUES (${customer.id}, ${customer.name}, ${customer.email}, ${customer.image_url}) 75 | ON CONFLICT (id) DO NOTHING; 76 | `, 77 | ), 78 | ); 79 | 80 | return insertedCustomers; 81 | } 82 | 83 | async function seedRevenue() { 84 | await client.sql` 85 | CREATE TABLE IF NOT EXISTS revenue ( 86 | month VARCHAR(4) NOT NULL UNIQUE, 87 | revenue INT NOT NULL 88 | ); 89 | `; 90 | 91 | const insertedRevenue = await Promise.all( 92 | revenue.map( 93 | (rev) => client.sql` 94 | INSERT INTO revenue (month, revenue) 95 | VALUES (${rev.month}, ${rev.revenue}) 96 | ON CONFLICT (month) DO NOTHING; 97 | `, 98 | ), 99 | ); 100 | 101 | return insertedRevenue; 102 | } 103 | 104 | export async function GET() { 105 | try { 106 | await client.sql`BEGIN`; 107 | await seedUsers(); 108 | await seedCustomers(); 109 | await seedInvoices(); 110 | await seedRevenue(); 111 | await client.sql`COMMIT`; 112 | 113 | return Response.json({ message: 'Database seeded successfully' }); 114 | } catch (error) { 115 | await client.sql`ROLLBACK`; 116 | return Response.json({ error }, { status: 500 }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/acme-logo.tsx: -------------------------------------------------------------------------------- 1 | import { GlobeAltIcon } from '@heroicons/react/24/outline'; 2 | import { lusitana } from '@/app/ui/fonts'; 3 | 4 | export default function AcmeLogo() { 5 | return ( 6 |
9 | 10 |

Acme

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | interface ButtonProps extends React.ButtonHTMLAttributes { 4 | children: React.ReactNode; 5 | } 6 | 7 | export function Button({ children, className, ...rest }: ButtonProps) { 8 | return ( 9 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/customers/table.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import { lusitana } from '@/app/ui/fonts'; 3 | import Search from '@/app/ui/search'; 4 | import { 5 | CustomersTableType, 6 | FormattedCustomersTable, 7 | } from '@/app/lib/definitions'; 8 | 9 | export default async function CustomersTable({ 10 | customers, 11 | }: { 12 | customers: FormattedCustomersTable[]; 13 | }) { 14 | return ( 15 |
16 |

17 | Customers 18 |

19 | 20 |
21 |
22 |
23 |
24 |
25 | {customers?.map((customer) => ( 26 |
30 |
31 |
32 |
33 |
34 | 41 |

{customer.name}

42 |
43 |
44 |

45 | {customer.email} 46 |

47 |
48 |
49 |
50 |
51 |

Pending

52 |

{customer.total_pending}

53 |
54 |
55 |

Paid

56 |

{customer.total_paid}

57 |
58 |
59 |
60 |

{customer.total_invoices} invoices

61 |
62 |
63 | ))} 64 |
65 |
66 | 67 | 68 | 71 | 74 | 77 | 80 | 83 | 84 | 85 | 86 | 87 | {customers.map((customer) => ( 88 | 89 | 101 | 104 | 107 | 110 | 113 | 114 | ))} 115 | 116 |
69 | Name 70 | 72 | Email 73 | 75 | Total Invoices 76 | 78 | Total Pending 79 | 81 | Total Paid 82 |
90 |
91 | {`${customer.name}'s 98 |

{customer.name}

99 |
100 |
102 | {customer.email} 103 | 105 | {customer.total_invoices} 106 | 108 | {customer.total_pending} 109 | 111 | {customer.total_paid} 112 |
117 |
118 | 119 | 120 | 121 | 122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/dashboard/cards.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BanknotesIcon, 3 | ClockIcon, 4 | UserGroupIcon, 5 | InboxIcon, 6 | } from '@heroicons/react/24/outline'; 7 | import { lusitana } from '@/app/ui/fonts'; 8 | import { fetchCardData } from '@/app/lib/data'; 9 | 10 | const iconMap = { 11 | collected: BanknotesIcon, 12 | customers: UserGroupIcon, 13 | pending: ClockIcon, 14 | invoices: InboxIcon, 15 | }; 16 | 17 | export default async function CardWrapper() { 18 | const { 19 | numberOfInvoices, 20 | numberOfCustomers, 21 | totalPaidInvoices, 22 | totalPendingInvoices, 23 | } = await fetchCardData(); 24 | return ( 25 | <> 26 | {/* NOTE: Uncomment this code in Chapter 9 */} 27 | 28 | 29 | 30 | 31 | 36 | 37 | ); 38 | } 39 | 40 | export function Card({ 41 | title, 42 | value, 43 | type, 44 | }: { 45 | title: string; 46 | value: number | string; 47 | type: 'invoices' | 'customers' | 'pending' | 'collected'; 48 | }) { 49 | const Icon = iconMap[type]; 50 | 51 | return ( 52 |
53 |
54 | {Icon ? : null} 55 |

{title}

56 |
57 |

61 | {value} 62 |

63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/dashboard/latest-invoices.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowPathIcon } from '@heroicons/react/24/outline'; 2 | import clsx from 'clsx'; 3 | import Image from 'next/image'; 4 | import { lusitana } from '@/app/ui/fonts'; 5 | import { LatestInvoice } from '@/app/lib/definitions'; 6 | import { fetchLatestInvoices } from '@/app/lib/data'; 7 | 8 | export default async function LatestInvoices() { 9 | const latestInvoices = await fetchLatestInvoices(); 10 | 11 | return ( 12 |
13 |

14 | Latest Invoices 15 |

16 |
17 | {/* NOTE: Uncomment this code in Chapter 7 */} 18 | 19 |
20 | {latestInvoices.map((invoice, i) => { 21 | return ( 22 |
31 |
32 | {`${invoice.name}'s 39 |
40 |

41 | {invoice.name} 42 |

43 |

44 | {invoice.email} 45 |

46 |
47 |
48 |

51 | {invoice.amount} 52 |

53 |
54 | ); 55 | })} 56 |
57 |
58 | 59 |

Updated just now

60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/dashboard/nav-links.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | UserGroupIcon, 5 | HomeIcon, 6 | DocumentDuplicateIcon, 7 | } from '@heroicons/react/24/outline'; 8 | import Link from 'next/link'; 9 | import { usePathname } from 'next/navigation'; 10 | import clsx from 'clsx'; 11 | 12 | // Map of links to display in the side navigation. 13 | // Depending on the size of the application, this would be stored in a database. 14 | const links = [ 15 | { name: 'Home', href: '/dashboard', icon: HomeIcon }, 16 | { 17 | name: 'Invoices', 18 | href: '/dashboard/invoices', 19 | icon: DocumentDuplicateIcon, 20 | }, 21 | { name: 'Customers', href: '/dashboard/customers', icon: UserGroupIcon }, 22 | ]; 23 | 24 | export default function NavLinks() { 25 | const pathname = usePathname(); 26 | 27 | return ( 28 | <> 29 | {links.map((link) => { 30 | const LinkIcon = link.icon; 31 | return ( 32 | 42 | 43 |

{link.name}

44 | 45 | ); 46 | })} 47 | 48 | ); 49 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/dashboard/revenue-chart.tsx: -------------------------------------------------------------------------------- 1 | import { generateYAxis } from '@/app/lib/utils'; 2 | import { CalendarIcon } from '@heroicons/react/24/outline'; 3 | import { lusitana } from '@/app/ui/fonts'; 4 | import { fetchRevenue } from '@/app/lib/data'; 5 | import { Revenue } from '@/app/lib/definitions'; 6 | 7 | // This component is representational only. 8 | // For data visualization UI, check out: 9 | // https://www.tremor.so/ 10 | // https://www.chartjs.org/ 11 | // https://airbnb.io/visx/ 12 | 13 | export default async function RevenueChart() { 14 | const revenue = await fetchRevenue(); 15 | 16 | const chartHeight = 350; 17 | // NOTE: Uncomment this code in Chapter 7 18 | 19 | const { yAxisLabels, topLabel } = generateYAxis(revenue); 20 | 21 | if (!revenue || revenue.length === 0) { 22 | return

No data available.

; 23 | } 24 | 25 | return ( 26 |
27 |

28 | Recent Revenue 29 |

30 | {/* NOTE: Uncomment this code in Chapter 7 */} 31 | 32 |
33 |
34 |
38 | {yAxisLabels.map((label) => ( 39 |

{label}

40 | ))} 41 |
42 | 43 | {revenue.map((month) => ( 44 |
45 |
51 |

52 | {month.month} 53 |

54 |
55 | ))} 56 |
57 |
58 | 59 |

Last 12 months

60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/dashboard/sidenav.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import NavLinks from '@/app/ui/dashboard/nav-links'; 3 | import AcmeLogo from '@/app/ui/acme-logo'; 4 | import { PowerIcon } from '@heroicons/react/24/outline'; 5 | import { handleSignOut } from '@/app/actions/auth'; 6 | 7 | export default function SideNav() { 8 | return ( 9 |
10 | // ... 11 |
12 | 13 |
14 | 15 | 19 | 20 |
21 |
22 | ); 23 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Inter } from 'next/font/google'; 2 | import { Lusitana } from 'next/font/google'; 3 | 4 | export const inter = Inter({ subsets: ['latin'] }); 5 | export const lusitana = Lusitana({ 6 | weight: ['400', '700'], 7 | subsets: ['latin'], 8 | }); -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | input[type='number'] { 6 | -moz-appearance: textfield; 7 | appearance: textfield; 8 | } 9 | 10 | input[type='number']::-webkit-inner-spin-button { 11 | -webkit-appearance: none; 12 | margin: 0; 13 | } 14 | 15 | input[type='number']::-webkit-outer-spin-button { 16 | -webkit-appearance: none; 17 | margin: 0; 18 | } 19 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/home.module.css: -------------------------------------------------------------------------------- 1 | .shape { 2 | height: 0; 3 | width: 0; 4 | border-bottom: 30px solid black; 5 | border-left: 20px solid transparent; 6 | border-right: 20px solid transparent; 7 | } -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/invoices/breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import { clsx } from 'clsx'; 2 | import Link from 'next/link'; 3 | import { lusitana } from '@/app/ui/fonts'; 4 | 5 | interface Breadcrumb { 6 | label: string; 7 | href: string; 8 | active?: boolean; 9 | } 10 | 11 | export default function Breadcrumbs({ 12 | breadcrumbs, 13 | }: { 14 | breadcrumbs: Breadcrumb[]; 15 | }) { 16 | return ( 17 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/invoices/buttons.tsx: -------------------------------------------------------------------------------- 1 | import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline'; 2 | import Link from 'next/link'; 3 | import { deleteInvoice } from '@/app/lib/actions'; 4 | 5 | export function CreateInvoice() { 6 | return ( 7 | 11 | Create Invoice{' '} 12 | 13 | 14 | ); 15 | } 16 | 17 | export function UpdateInvoice({ id }: { id: string }) { 18 | return ( 19 | 23 | 24 | 25 | ); 26 | } 27 | 28 | export function DeleteInvoice({ id }: { id: string }) { 29 | const deleteInvoiceWithId = deleteInvoice.bind(null, id); 30 | 31 | return ( 32 |
33 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/invoices/create-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { CustomerField } from '@/app/lib/definitions'; 4 | import Link from 'next/link'; 5 | import { 6 | CheckIcon, 7 | ClockIcon, 8 | CurrencyDollarIcon, 9 | UserCircleIcon, 10 | } from '@heroicons/react/24/outline'; 11 | import { Button } from '@/app/ui/button'; 12 | import { createInvoice, State } from '@/app/lib/actions'; 13 | import { useActionState } from 'react'; 14 | 15 | export default function Form({ customers }: { customers: CustomerField[] }) { 16 | const initialState = { 17 | message: null, 18 | errors: {}, 19 | }; 20 | const [state, formAction] = useActionState(createInvoice, initialState) 21 | 22 | return ( 23 |
24 |
25 | {/* Customer Name */} 26 |
27 | 30 |
31 | 47 | 48 |
49 |
50 | {state.errors?.customerId && state.errors.customerId.map((error: string) => ( 51 |

52 | {error} 53 |

54 | ))} 55 |
56 |
57 | 58 | {/* Invoice Amount */} 59 |
60 | 63 |
64 |
65 | 73 | 74 |
75 |
76 |
77 | 78 | {/* Invoice Status */} 79 |
80 | 81 | Set the invoice status 82 | 83 |
84 |
85 |
86 | 93 | 99 |
100 |
101 | 108 | 114 |
115 |
116 |
117 |
118 |
119 |
120 | 124 | Cancel 125 | 126 | 127 |
128 |
129 | ); 130 | } 131 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/invoices/edit-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { CustomerField, InvoiceForm } from '@/app/lib/definitions'; 4 | import { 5 | CheckIcon, 6 | ClockIcon, 7 | CurrencyDollarIcon, 8 | UserCircleIcon, 9 | } from '@heroicons/react/24/outline'; 10 | import Link from 'next/link'; 11 | import { Button } from '@/app/ui/button'; 12 | import { updateInvoice } from '@/app/lib/actions'; 13 | import { useActionState } from 'react'; 14 | 15 | export default function EditInvoiceForm({ 16 | invoice, 17 | customers, 18 | }: { 19 | invoice: InvoiceForm; 20 | customers: CustomerField[]; 21 | }) { 22 | const initialState = { 23 | message: null, 24 | errors: {}, 25 | }; 26 | const updateInvoiceWithId = updateInvoice.bind(null, invoice.id); 27 | const [state, formAction] = useActionState(updateInvoiceWithId, initialState); 28 | 29 | return ( 30 |
31 |
32 | {/* Customer Name */} 33 |
34 | 37 |
38 | 53 | 54 |
55 |
56 | 57 | {/* Invoice Amount */} 58 |
59 | 62 |
63 |
64 | 73 | 74 |
75 |
76 |
77 | 78 | {/* Invoice Status */} 79 |
80 | 81 | Set the invoice status 82 | 83 |
84 |
85 |
86 | 94 | 100 |
101 |
102 | 110 | 116 |
117 |
118 |
119 |
120 |
121 |
122 | 126 | Cancel 127 | 128 | 129 |
130 |
131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/invoices/pagination.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'; 4 | import clsx from 'clsx'; 5 | import Link from 'next/link'; 6 | import { generatePagination } from '@/app/lib/utils'; 7 | import { usePathname, useRouter, useSearchParams } from 'next/navigation'; 8 | 9 | export default function Pagination({ totalPages }: { totalPages: number }) { 10 | // NOTE: Uncomment this code in Chapter 11 11 | const pathname = usePathname(); 12 | const searchParams = useSearchParams(); 13 | const currentPage = Number(searchParams.get('page')) || 1; 14 | const allPages = generatePagination(currentPage, totalPages); 15 | 16 | const createPageURL = (pageNumber: number | string) => { 17 | const params = new URLSearchParams(searchParams); 18 | params.set('page', pageNumber.toString()); 19 | return `${pathname}?${params.toString()}`; 20 | }; 21 | 22 | return ( 23 | <> 24 | {/* NOTE: Uncomment this code in Chapter 11 */} 25 | 26 |
27 | 32 | 33 |
34 | {allPages.map((page, index) => { 35 | let position: 'first' | 'last' | 'single' | 'middle' | undefined; 36 | 37 | if (index === 0) position = 'first'; 38 | if (index === allPages.length - 1) position = 'last'; 39 | if (allPages.length === 1) position = 'single'; 40 | if (page === '...') position = 'middle'; 41 | 42 | return ( 43 | 50 | ); 51 | })} 52 |
53 | 54 | = totalPages} 58 | /> 59 |
60 | 61 | ); 62 | } 63 | 64 | function PaginationNumber({ 65 | page, 66 | href, 67 | isActive, 68 | position, 69 | }: { 70 | page: number | string; 71 | href: string; 72 | position?: 'first' | 'last' | 'middle' | 'single'; 73 | isActive: boolean; 74 | }) { 75 | const className = clsx( 76 | 'flex h-10 w-10 items-center justify-center text-sm border', 77 | { 78 | 'rounded-l-md': position === 'first' || position === 'single', 79 | 'rounded-r-md': position === 'last' || position === 'single', 80 | 'z-10 bg-blue-600 border-blue-600 text-white': isActive, 81 | 'hover:bg-gray-100': !isActive && position !== 'middle', 82 | 'text-gray-300': position === 'middle', 83 | }, 84 | ); 85 | 86 | return isActive || position === 'middle' ? ( 87 |
{page}
88 | ) : ( 89 | 90 | {page} 91 | 92 | ); 93 | } 94 | 95 | function PaginationArrow({ 96 | href, 97 | direction, 98 | isDisabled, 99 | }: { 100 | href: string; 101 | direction: 'left' | 'right'; 102 | isDisabled?: boolean; 103 | }) { 104 | const className = clsx( 105 | 'flex h-10 w-10 items-center justify-center rounded-md border', 106 | { 107 | 'pointer-events-none text-gray-300': isDisabled, 108 | 'hover:bg-gray-100': !isDisabled, 109 | 'mr-2 md:mr-4': direction === 'left', 110 | 'ml-2 md:ml-4': direction === 'right', 111 | }, 112 | ); 113 | 114 | const icon = 115 | direction === 'left' ? ( 116 | 117 | ) : ( 118 | 119 | ); 120 | 121 | return isDisabled ? ( 122 |
{icon}
123 | ) : ( 124 | 125 | {icon} 126 | 127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/invoices/status.tsx: -------------------------------------------------------------------------------- 1 | import { CheckIcon, ClockIcon } from '@heroicons/react/24/outline'; 2 | import clsx from 'clsx'; 3 | 4 | export default function InvoiceStatus({ status }: { status: string }) { 5 | return ( 6 | 15 | {status === 'pending' ? ( 16 | <> 17 | Pending 18 | 19 | 20 | ) : null} 21 | {status === 'paid' ? ( 22 | <> 23 | Paid 24 | 25 | 26 | ) : null} 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/invoices/table.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import { UpdateInvoice, DeleteInvoice } from '@/app/ui/invoices/buttons'; 3 | import InvoiceStatus from '@/app/ui/invoices/status'; 4 | import { formatDateToLocal, formatCurrency } from '@/app/lib/utils'; 5 | import { fetchFilteredInvoices } from '@/app/lib/data'; 6 | 7 | export default async function InvoicesTable({ 8 | query, 9 | currentPage, 10 | }: { 11 | query: string; 12 | currentPage: number; 13 | }) { 14 | const invoices = await fetchFilteredInvoices(query, currentPage); 15 | 16 | return ( 17 |
18 |
19 |
20 |
21 | {invoices?.map((invoice) => ( 22 |
26 |
27 |
28 |
29 | {`${invoice.name}'s 36 |

{invoice.name}

37 |
38 |

{invoice.email}

39 |
40 | 41 |
42 |
43 |
44 |

45 | {formatCurrency(invoice.amount)} 46 |

47 |

{formatDateToLocal(invoice.date)}

48 |
49 |
50 | 51 | 52 |
53 |
54 |
55 | ))} 56 |
57 | 58 | 59 | 60 | 63 | 66 | 69 | 72 | 75 | 78 | 79 | 80 | 81 | {invoices?.map((invoice) => ( 82 | 86 | 98 | 101 | 104 | 107 | 110 | 116 | 117 | ))} 118 | 119 |
61 | Customer 62 | 64 | Email 65 | 67 | Amount 68 | 70 | Date 71 | 73 | Status 74 | 76 | Edit 77 |
87 |
88 | {`${invoice.name}'s 95 |

{invoice.name}

96 |
97 |
99 | {invoice.email} 100 | 102 | {formatCurrency(invoice.amount)} 103 | 105 | {formatDateToLocal(invoice.date)} 106 | 108 | 109 | 111 |
112 | 113 | 114 |
115 |
120 |
121 |
122 |
123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/login-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { lusitana } from '@/app/ui/fonts'; 4 | import { useFormState } from 'react-dom'; 5 | import { authenticate } from '@/app/lib/actions'; 6 | import { 7 | AtSymbolIcon, 8 | KeyIcon, 9 | ExclamationCircleIcon, 10 | } from '@heroicons/react/24/outline'; 11 | import { ArrowRightIcon } from '@heroicons/react/20/solid'; 12 | import { Button } from './button'; 13 | 14 | export default function LoginForm() { 15 | const [errorMessage, dispatch] = useFormState(authenticate, undefined); 16 | 17 | return ( 18 |
19 |
20 |

21 | Please log in to continue. 22 |

23 |
24 |
25 | 31 |
32 | 40 | 41 |
42 |
43 |
44 | 50 |
51 | 60 | 61 |
62 |
63 |
64 | 67 |
68 | {errorMessage && ( 69 | <> 70 | 71 |

{errorMessage}

72 | 73 | )} 74 |
75 |
76 |
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/search.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; 4 | import { useSearchParams, usePathname, useRouter } from 'next/navigation'; 5 | import { useDebouncedCallback } from 'use-debounce'; 6 | 7 | export default function Search({ placeholder }: { placeholder: string }) { 8 | 9 | const searchParams = useSearchParams(); 10 | const pathname = usePathname(); 11 | const {replace} = useRouter(); 12 | 13 | const handleSearch = useDebouncedCallback((term: string) => { 14 | console.log(`Searching... ${term}`); 15 | const params = new URLSearchParams(searchParams); 16 | params.set('page', '1'); 17 | if (term) { 18 | params.set('query', term); 19 | } else { 20 | params.delete('query'); 21 | } 22 | replace(`${pathname}?${params.toString()}`); 23 | }, 300); 24 | 25 | return ( 26 |
27 | 30 | handleSearch(e.target.value)} 34 | defaultValue={searchParams.get('query')?.toString()} 35 | /> 36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/app/ui/skeletons.tsx: -------------------------------------------------------------------------------- 1 | // Loading animation 2 | const shimmer = 3 | 'before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/60 before:to-transparent'; 4 | 5 | export function CardSkeleton() { 6 | return ( 7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ); 19 | } 20 | 21 | export function CardsSkeleton() { 22 | return ( 23 | <> 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | export function RevenueChartSkeleton() { 33 | return ( 34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | ); 45 | } 46 | 47 | export function InvoiceSkeleton() { 48 | return ( 49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | ); 60 | } 61 | 62 | export function LatestInvoicesSkeleton() { 63 | return ( 64 |
67 |
68 |
69 |
70 | 71 | 72 | 73 | 74 | 75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | ); 83 | } 84 | 85 | export default function DashboardSkeleton() { 86 | return ( 87 | <> 88 |
91 |
92 | 93 | 94 | 95 | 96 |
97 |
98 | 99 | 100 |
101 | 102 | ); 103 | } 104 | 105 | export function TableRowSkeleton() { 106 | return ( 107 | 108 | {/* Customer Name and Image */} 109 | 110 |
111 |
112 |
113 |
114 | 115 | {/* Email */} 116 | 117 |
118 | 119 | {/* Amount */} 120 | 121 |
122 | 123 | {/* Date */} 124 | 125 |
126 | 127 | {/* Status */} 128 | 129 |
130 | 131 | {/* Actions */} 132 | 133 |
134 |
135 |
136 |
137 | 138 | 139 | ); 140 | } 141 | 142 | export function InvoicesMobileSkeleton() { 143 | return ( 144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | ); 164 | } 165 | 166 | export function InvoicesTableSkeleton() { 167 | return ( 168 |
169 |
170 |
171 |
172 | 173 | 174 | 175 | 176 | 177 | 178 |
179 | 180 | 181 | 182 | 185 | 188 | 191 | 194 | 197 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 |
183 | Customer 184 | 186 | Email 187 | 189 | Amount 190 | 192 | Date 193 | 195 | Status 196 | 201 | Edit 202 |
214 |
215 |
216 |
217 | ); 218 | } 219 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/auth.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextAuthConfig } from 'next-auth'; 2 | 3 | export const authConfig = { 4 | pages: { 5 | signIn: '/login', 6 | }, 7 | callbacks: { 8 | authorized({ auth, request: { nextUrl } }) { 9 | const isLoggedIn = !!auth?.user; 10 | const isOnDashboard = nextUrl.pathname.startsWith('/dashboard'); 11 | if (isOnDashboard) { 12 | if (isLoggedIn) return true; 13 | return false; // Redirect unauthenticated users to login page 14 | } else if (isLoggedIn) { 15 | return Response.redirect(new URL('/dashboard', nextUrl)); 16 | } 17 | return true; 18 | }, 19 | }, 20 | providers: [], // Add providers with an empty array for now 21 | } satisfies NextAuthConfig; -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import Credentials from 'next-auth/providers/credentials'; 3 | import { authConfig } from './auth.config'; 4 | import { z } from 'zod'; 5 | import { sql } from '@vercel/postgres'; 6 | import type { User } from '@/app/lib/definitions'; 7 | import bcrypt from 'bcrypt'; 8 | 9 | async function getUser(email: string): Promise { 10 | try { 11 | const user = await sql`SELECT * FROM users WHERE email=${email}`; 12 | return user.rows[0]; 13 | } catch (error) { 14 | console.error('Failed to fetch user:', error); 15 | throw new Error('Failed to fetch user.'); 16 | } 17 | } 18 | 19 | export const { auth, signIn, signOut } = NextAuth({ 20 | ...authConfig, 21 | providers: [ 22 | Credentials({ 23 | async authorize(credentials) { 24 | const parsedCredentials = z 25 | .object({ email: z.string().email(), password: z.string().min(6) }) 26 | .safeParse(credentials); 27 | 28 | if (parsedCredentials.success) { 29 | const { email, password } = parsedCredentials.data; 30 | const user = await getUser(email); 31 | if (!user) return null; 32 | const passwordMatch = await bcrypt.compare(password, user.password); 33 | if (passwordMatch) return user; 34 | } 35 | 36 | console.log('Invalid email or password'); 37 | return null; 38 | }, 39 | }), 40 | ], 41 | }); -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/middleware.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import { authConfig } from './auth.config'; 3 | 4 | export default NextAuth(authConfig).auth; 5 | 6 | export const config = { 7 | // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher 8 | matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], 9 | }; -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const nextConfig = { 4 | experimental: { 5 | ppr: 'incremental', 6 | }, 7 | }; 8 | 9 | export default nextConfig; 10 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "next build", 5 | "dev": "next dev", 6 | "start": "next start", 7 | "lint": "next lint" 8 | }, 9 | "dependencies": { 10 | "@heroicons/react": "^2.1.4", 11 | "@tailwindcss/forms": "^0.5.7", 12 | "@vercel/postgres": "^0.8.0", 13 | "autoprefixer": "10.4.19", 14 | "bcrypt": "^5.1.1", 15 | "clsx": "^2.1.1", 16 | "next": "15.0.0-canary.56", 17 | "next-auth": "5.0.0-beta.20", 18 | "postcss": "8.4.38", 19 | "react": "19.0.0-rc-f38c22b244-20240704", 20 | "react-dom": "19.0.0-rc-f38c22b244-20240704", 21 | "tailwindcss": "3.4.4", 22 | "typescript": "5.5.2", 23 | "use-debounce": "^10.0.1", 24 | "zod": "^3.23.8" 25 | }, 26 | "devDependencies": { 27 | "@types/bcrypt": "^5.0.2", 28 | "@types/node": "20.14.8", 29 | "@types/react": "18.3.3", 30 | "@types/react-dom": "18.3.0", 31 | "@typescript-eslint/eslint-plugin": "^8.4.0", 32 | "@typescript-eslint/parser": "^8.4.0", 33 | "eslint": "^8", 34 | "eslint-config-next": "14.2.7" 35 | }, 36 | "engines": { 37 | "node": ">=20.12.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/public/customers/amy-burns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/.learning/nextjs/nextjs-dashboard/public/customers/amy-burns.png -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/public/customers/balazs-orban.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/.learning/nextjs/nextjs-dashboard/public/customers/balazs-orban.png -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/public/customers/delba-de-oliveira.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/.learning/nextjs/nextjs-dashboard/public/customers/delba-de-oliveira.png -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/public/customers/evil-rabbit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/.learning/nextjs/nextjs-dashboard/public/customers/evil-rabbit.png -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/public/customers/lee-robinson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/.learning/nextjs/nextjs-dashboard/public/customers/lee-robinson.png -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/public/customers/michael-novotny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/.learning/nextjs/nextjs-dashboard/public/customers/michael-novotny.png -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/public/hero-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/.learning/nextjs/nextjs-dashboard/public/hero-desktop.png -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/public/hero-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/.learning/nextjs/nextjs-dashboard/public/hero-mobile.png -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | gridTemplateColumns: { 12 | '13': 'repeat(13, minmax(0, 1fr))', 13 | }, 14 | colors: { 15 | blue: { 16 | 400: '#2589FE', 17 | 500: '#0070F3', 18 | 600: '#2F6FEB', 19 | }, 20 | }, 21 | }, 22 | keyframes: { 23 | shimmer: { 24 | '100%': { 25 | transform: 'translateX(100%)', 26 | }, 27 | }, 28 | }, 29 | }, 30 | plugins: [require('@tailwindcss/forms')], 31 | }; 32 | export default config; 33 | -------------------------------------------------------------------------------- /.learning/nextjs/nextjs-dashboard/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 | "baseUrl": ".", 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": [ 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx", 30 | ".next/types/**/*.ts", 31 | "app/lib/placeholder-data.ts", 32 | "scripts/seed.js" 33 | ], 34 | "exclude": ["node_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /.learning/react/app/layout.js: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: 'Next.js', 3 | description: 'Generated by Next.js', 4 | } 5 | 6 | export default function RootLayout({ children }) { 7 | return ( 8 | 9 | {children} 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /.learning/react/app/like-button.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react'; 4 | 5 | export default function LikeButton() { 6 | const [likes, setLikes] = useState(0); 7 | 8 | function handleClick() { 9 | setLikes(likes + 1); 10 | } 11 | 12 | return 13 | } -------------------------------------------------------------------------------- /.learning/react/app/page.jsx: -------------------------------------------------------------------------------- 1 | import LikeButton from './like-button'; 2 | 3 | function Header({ title }) { 4 | return

{title ? title : 'Default title'}

; 5 | } 6 | 7 | function HomePage() { 8 | const names = ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton']; 9 | 10 | return ( 11 |
12 |
13 |
    14 | {names.map((name) => ( 15 |
  • {name}
  • 16 | ))} 17 |
18 | 19 | 20 |
21 | ); 22 | } 23 | 24 | export default HomePage; 25 | -------------------------------------------------------------------------------- /.learning/react/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "next": "^14.2.7", 9 | "react": "^18.3.1", 10 | "react-dom": "^18.3.1" 11 | } 12 | }, 13 | "node_modules/@next/env": { 14 | "version": "14.2.7", 15 | "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.7.tgz", 16 | "integrity": "sha512-OTx9y6I3xE/eih+qtthppwLytmpJVPM5PPoJxChFsbjIEFXIayG0h/xLzefHGJviAa3Q5+Fd+9uYojKkHDKxoQ==", 17 | "license": "MIT" 18 | }, 19 | "node_modules/@next/swc-darwin-arm64": { 20 | "version": "14.2.7", 21 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.7.tgz", 22 | "integrity": "sha512-UhZGcOyI9LE/tZL3h9rs/2wMZaaJKwnpAyegUVDGZqwsla6hMfeSj9ssBWQS9yA4UXun3pPhrFLVnw5KXZs3vw==", 23 | "cpu": [ 24 | "arm64" 25 | ], 26 | "license": "MIT", 27 | "optional": true, 28 | "os": [ 29 | "darwin" 30 | ], 31 | "engines": { 32 | "node": ">= 10" 33 | } 34 | }, 35 | "node_modules/@next/swc-darwin-x64": { 36 | "version": "14.2.7", 37 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.7.tgz", 38 | "integrity": "sha512-ys2cUgZYRc+CbyDeLAaAdZgS7N1Kpyy+wo0b/gAj+SeOeaj0Lw/q+G1hp+DuDiDAVyxLBCJXEY/AkhDmtihUTA==", 39 | "cpu": [ 40 | "x64" 41 | ], 42 | "license": "MIT", 43 | "optional": true, 44 | "os": [ 45 | "darwin" 46 | ], 47 | "engines": { 48 | "node": ">= 10" 49 | } 50 | }, 51 | "node_modules/@next/swc-linux-arm64-gnu": { 52 | "version": "14.2.7", 53 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.7.tgz", 54 | "integrity": "sha512-2xoWtE13sUJ3qrC1lwE/HjbDPm+kBQYFkkiVECJWctRASAHQ+NwjMzgrfqqMYHfMxFb5Wws3w9PqzZJqKFdWcQ==", 55 | "cpu": [ 56 | "arm64" 57 | ], 58 | "license": "MIT", 59 | "optional": true, 60 | "os": [ 61 | "linux" 62 | ], 63 | "engines": { 64 | "node": ">= 10" 65 | } 66 | }, 67 | "node_modules/@next/swc-linux-arm64-musl": { 68 | "version": "14.2.7", 69 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.7.tgz", 70 | "integrity": "sha512-+zJ1gJdl35BSAGpkCbfyiY6iRTaPrt3KTl4SF/B1NyELkqqnrNX6cp4IjjjxKpd64/7enI0kf6b9O1Uf3cL0pw==", 71 | "cpu": [ 72 | "arm64" 73 | ], 74 | "license": "MIT", 75 | "optional": true, 76 | "os": [ 77 | "linux" 78 | ], 79 | "engines": { 80 | "node": ">= 10" 81 | } 82 | }, 83 | "node_modules/@next/swc-linux-x64-gnu": { 84 | "version": "14.2.7", 85 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.7.tgz", 86 | "integrity": "sha512-m6EBqrskeMUzykBrv0fDX/28lWIBGhMzOYaStp0ihkjzIYJiKUOzVYD1gULHc8XDf5EMSqoH/0/TRAgXqpQwmw==", 87 | "cpu": [ 88 | "x64" 89 | ], 90 | "license": "MIT", 91 | "optional": true, 92 | "os": [ 93 | "linux" 94 | ], 95 | "engines": { 96 | "node": ">= 10" 97 | } 98 | }, 99 | "node_modules/@next/swc-linux-x64-musl": { 100 | "version": "14.2.7", 101 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.7.tgz", 102 | "integrity": "sha512-gUu0viOMvMlzFRz1r1eQ7Ql4OE+hPOmA7smfZAhn8vC4+0swMZaZxa9CSIozTYavi+bJNDZ3tgiSdMjmMzRJlQ==", 103 | "cpu": [ 104 | "x64" 105 | ], 106 | "license": "MIT", 107 | "optional": true, 108 | "os": [ 109 | "linux" 110 | ], 111 | "engines": { 112 | "node": ">= 10" 113 | } 114 | }, 115 | "node_modules/@next/swc-win32-arm64-msvc": { 116 | "version": "14.2.7", 117 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.7.tgz", 118 | "integrity": "sha512-PGbONHIVIuzWlYmLvuFKcj+8jXnLbx4WrlESYlVnEzDsa3+Q2hI1YHoXaSmbq0k4ZwZ7J6sWNV4UZfx1OeOlbQ==", 119 | "cpu": [ 120 | "arm64" 121 | ], 122 | "license": "MIT", 123 | "optional": true, 124 | "os": [ 125 | "win32" 126 | ], 127 | "engines": { 128 | "node": ">= 10" 129 | } 130 | }, 131 | "node_modules/@next/swc-win32-ia32-msvc": { 132 | "version": "14.2.7", 133 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.7.tgz", 134 | "integrity": "sha512-BiSY5umlx9ed5RQDoHcdbuKTUkuFORDqzYKPHlLeS+STUWQKWziVOn3Ic41LuTBvqE0TRJPKpio9GSIblNR+0w==", 135 | "cpu": [ 136 | "ia32" 137 | ], 138 | "license": "MIT", 139 | "optional": true, 140 | "os": [ 141 | "win32" 142 | ], 143 | "engines": { 144 | "node": ">= 10" 145 | } 146 | }, 147 | "node_modules/@next/swc-win32-x64-msvc": { 148 | "version": "14.2.7", 149 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.7.tgz", 150 | "integrity": "sha512-pxsI23gKWRt/SPHFkDEsP+w+Nd7gK37Hpv0ngc5HpWy2e7cKx9zR/+Q2ptAUqICNTecAaGWvmhway7pj/JLEWA==", 151 | "cpu": [ 152 | "x64" 153 | ], 154 | "license": "MIT", 155 | "optional": true, 156 | "os": [ 157 | "win32" 158 | ], 159 | "engines": { 160 | "node": ">= 10" 161 | } 162 | }, 163 | "node_modules/@swc/counter": { 164 | "version": "0.1.3", 165 | "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", 166 | "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", 167 | "license": "Apache-2.0" 168 | }, 169 | "node_modules/@swc/helpers": { 170 | "version": "0.5.5", 171 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", 172 | "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", 173 | "license": "Apache-2.0", 174 | "dependencies": { 175 | "@swc/counter": "^0.1.3", 176 | "tslib": "^2.4.0" 177 | } 178 | }, 179 | "node_modules/busboy": { 180 | "version": "1.6.0", 181 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 182 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 183 | "dependencies": { 184 | "streamsearch": "^1.1.0" 185 | }, 186 | "engines": { 187 | "node": ">=10.16.0" 188 | } 189 | }, 190 | "node_modules/caniuse-lite": { 191 | "version": "1.0.30001655", 192 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz", 193 | "integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==", 194 | "funding": [ 195 | { 196 | "type": "opencollective", 197 | "url": "https://opencollective.com/browserslist" 198 | }, 199 | { 200 | "type": "tidelift", 201 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 202 | }, 203 | { 204 | "type": "github", 205 | "url": "https://github.com/sponsors/ai" 206 | } 207 | ], 208 | "license": "CC-BY-4.0" 209 | }, 210 | "node_modules/client-only": { 211 | "version": "0.0.1", 212 | "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 213 | "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", 214 | "license": "MIT" 215 | }, 216 | "node_modules/graceful-fs": { 217 | "version": "4.2.11", 218 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 219 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 220 | "license": "ISC" 221 | }, 222 | "node_modules/js-tokens": { 223 | "version": "4.0.0", 224 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 225 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 226 | "license": "MIT" 227 | }, 228 | "node_modules/loose-envify": { 229 | "version": "1.4.0", 230 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 231 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 232 | "license": "MIT", 233 | "dependencies": { 234 | "js-tokens": "^3.0.0 || ^4.0.0" 235 | }, 236 | "bin": { 237 | "loose-envify": "cli.js" 238 | } 239 | }, 240 | "node_modules/nanoid": { 241 | "version": "3.3.7", 242 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 243 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 244 | "funding": [ 245 | { 246 | "type": "github", 247 | "url": "https://github.com/sponsors/ai" 248 | } 249 | ], 250 | "license": "MIT", 251 | "bin": { 252 | "nanoid": "bin/nanoid.cjs" 253 | }, 254 | "engines": { 255 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 256 | } 257 | }, 258 | "node_modules/next": { 259 | "version": "14.2.7", 260 | "resolved": "https://registry.npmjs.org/next/-/next-14.2.7.tgz", 261 | "integrity": "sha512-4Qy2aK0LwH4eQiSvQWyKuC7JXE13bIopEQesWE0c/P3uuNRnZCQanI0vsrMLmUQJLAto+A+/8+sve2hd+BQuOQ==", 262 | "license": "MIT", 263 | "dependencies": { 264 | "@next/env": "14.2.7", 265 | "@swc/helpers": "0.5.5", 266 | "busboy": "1.6.0", 267 | "caniuse-lite": "^1.0.30001579", 268 | "graceful-fs": "^4.2.11", 269 | "postcss": "8.4.31", 270 | "styled-jsx": "5.1.1" 271 | }, 272 | "bin": { 273 | "next": "dist/bin/next" 274 | }, 275 | "engines": { 276 | "node": ">=18.17.0" 277 | }, 278 | "optionalDependencies": { 279 | "@next/swc-darwin-arm64": "14.2.7", 280 | "@next/swc-darwin-x64": "14.2.7", 281 | "@next/swc-linux-arm64-gnu": "14.2.7", 282 | "@next/swc-linux-arm64-musl": "14.2.7", 283 | "@next/swc-linux-x64-gnu": "14.2.7", 284 | "@next/swc-linux-x64-musl": "14.2.7", 285 | "@next/swc-win32-arm64-msvc": "14.2.7", 286 | "@next/swc-win32-ia32-msvc": "14.2.7", 287 | "@next/swc-win32-x64-msvc": "14.2.7" 288 | }, 289 | "peerDependencies": { 290 | "@opentelemetry/api": "^1.1.0", 291 | "@playwright/test": "^1.41.2", 292 | "react": "^18.2.0", 293 | "react-dom": "^18.2.0", 294 | "sass": "^1.3.0" 295 | }, 296 | "peerDependenciesMeta": { 297 | "@opentelemetry/api": { 298 | "optional": true 299 | }, 300 | "@playwright/test": { 301 | "optional": true 302 | }, 303 | "sass": { 304 | "optional": true 305 | } 306 | } 307 | }, 308 | "node_modules/picocolors": { 309 | "version": "1.0.1", 310 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", 311 | "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", 312 | "license": "ISC" 313 | }, 314 | "node_modules/postcss": { 315 | "version": "8.4.31", 316 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", 317 | "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", 318 | "funding": [ 319 | { 320 | "type": "opencollective", 321 | "url": "https://opencollective.com/postcss/" 322 | }, 323 | { 324 | "type": "tidelift", 325 | "url": "https://tidelift.com/funding/github/npm/postcss" 326 | }, 327 | { 328 | "type": "github", 329 | "url": "https://github.com/sponsors/ai" 330 | } 331 | ], 332 | "license": "MIT", 333 | "dependencies": { 334 | "nanoid": "^3.3.6", 335 | "picocolors": "^1.0.0", 336 | "source-map-js": "^1.0.2" 337 | }, 338 | "engines": { 339 | "node": "^10 || ^12 || >=14" 340 | } 341 | }, 342 | "node_modules/react": { 343 | "version": "18.3.1", 344 | "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", 345 | "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", 346 | "license": "MIT", 347 | "dependencies": { 348 | "loose-envify": "^1.1.0" 349 | }, 350 | "engines": { 351 | "node": ">=0.10.0" 352 | } 353 | }, 354 | "node_modules/react-dom": { 355 | "version": "18.3.1", 356 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", 357 | "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", 358 | "license": "MIT", 359 | "dependencies": { 360 | "loose-envify": "^1.1.0", 361 | "scheduler": "^0.23.2" 362 | }, 363 | "peerDependencies": { 364 | "react": "^18.3.1" 365 | } 366 | }, 367 | "node_modules/scheduler": { 368 | "version": "0.23.2", 369 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", 370 | "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", 371 | "license": "MIT", 372 | "dependencies": { 373 | "loose-envify": "^1.1.0" 374 | } 375 | }, 376 | "node_modules/source-map-js": { 377 | "version": "1.2.0", 378 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", 379 | "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", 380 | "license": "BSD-3-Clause", 381 | "engines": { 382 | "node": ">=0.10.0" 383 | } 384 | }, 385 | "node_modules/streamsearch": { 386 | "version": "1.1.0", 387 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 388 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", 389 | "engines": { 390 | "node": ">=10.0.0" 391 | } 392 | }, 393 | "node_modules/styled-jsx": { 394 | "version": "5.1.1", 395 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", 396 | "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", 397 | "license": "MIT", 398 | "dependencies": { 399 | "client-only": "0.0.1" 400 | }, 401 | "engines": { 402 | "node": ">= 12.0.0" 403 | }, 404 | "peerDependencies": { 405 | "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" 406 | }, 407 | "peerDependenciesMeta": { 408 | "@babel/core": { 409 | "optional": true 410 | }, 411 | "babel-plugin-macros": { 412 | "optional": true 413 | } 414 | } 415 | }, 416 | "node_modules/tslib": { 417 | "version": "2.7.0", 418 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", 419 | "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", 420 | "license": "0BSD" 421 | } 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /.learning/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "next dev" 4 | }, 5 | "dependencies": { 6 | "next": "^14.2.7", 7 | "react": "^18.3.1", 8 | "react-dom": "^18.3.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Eugene Yan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building a small app with various frameworks 2 | 3 | My current approach of building apps is based on fastapi + jinja/html + css + some js. 4 | - Pros: Reliable stack that I'm familiar with 5 | - Cons: Outdated? 6 | 7 | Recently, I've been thinking about building apps in typescript/fastHTML and ran a poll [here](https://x.com/eugeneyan/status/1828447283811402006). This is an extension of the poll, where I'll build the same app five times using FastAPI + HTML, FastHTML, Next.js, and Svelte. The intent was to understand which suited me best, both in terms of ease of development, learning, and fun. 8 | 9 | The app will enable users to: 10 | - `upload` a csv file to initialize a sqlite table 11 | - `view` the table via the browser 12 | - `update` a row of the sqlite database 13 | - `delete` a row of the sqlite database 14 | - `download` the table of the sqlite table 15 | 16 | To keep things simple for this comparison, we'll: 17 | - Have a single table for the data 18 | - Not have features for table deletes or overwrites once the single table has been initialized 19 | 20 | > Also see the detailed writeup and some thoughts [here](https://eugeneyan.com/writing/web-frameworks/). 21 | 22 | ## Setup 23 | ``` 24 | # Install Python + FastAPI + FastHTML 25 | # Install uv: https://docs.astral.sh/uv/getting-started/installation/ 26 | uv init # Create a new python project 27 | uv sync # Install dependencies 28 | 29 | # Install Next.js + Svelte 30 | npm install -g pnpm # Install pnpm: https://pnpm.io 31 | 32 | # Next.js 33 | cd nextjs 34 | pnpm install # Install dependencies 35 | 36 | # Svelte 37 | cd svelte 38 | pnpm install # Install dependencies 39 | ``` 40 | 41 | ## Running the apps 42 | 43 | ### FastAPI + Jinja + CSS + JS 44 | ``` 45 | cd fastapi 46 | uv run uvicorn main:app --reload 47 | # Go to http://localhost:8000/ 48 | ``` 49 | 50 | ### FastHTML 51 | ``` 52 | cd fasthtml 53 | uv run python main.py 54 | # Go to http://localhost:5001 55 | ``` 56 | 57 | ### Next.js 58 | ``` 59 | cd nextjs 60 | pnpm run dev 61 | # Go to http://localhost:3000/ 62 | ``` 63 | 64 | ### Svelte 65 | ``` 66 | cd svelte 67 | pnpm run dev 68 | # Go to http://localhost:5173/ 69 | ``` 70 | 71 | ### FastAPI + Svelte 72 | ``` 73 | cd fastapi+svelte 74 | uv run uvicorn main:app --reload 75 | 76 | # Open another terminal 77 | cd svelte-app 78 | pnpm run dev 79 | 80 | # Go to http://localhost:5173/ 81 | ``` -------------------------------------------------------------------------------- /fastapi+svelte/database.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/fastapi+svelte/database.sqlite -------------------------------------------------------------------------------- /fastapi+svelte/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | 5 | def get_logger(name: str = __name__, level: str = "INFO") -> logging.Logger: 6 | """ 7 | Initialize a simple logger that outputs to the console. 8 | 9 | Args: 10 | name (str): The name of the logger. Defaults to the module name. 11 | level (str): The logging level. Defaults to "INFO". 12 | 13 | Returns: 14 | logging.Logger: A configured logger instance. 15 | """ 16 | # Create a logger 17 | logger = logging.getLogger(name) 18 | 19 | # Set the logging level 20 | level = getattr(logging, level.upper(), logging.INFO) 21 | logger.setLevel(level) 22 | 23 | # Create a console handler and set its level 24 | console_handler = logging.StreamHandler(sys.stdout) 25 | console_handler.setLevel(level) 26 | 27 | # Create a formatter 28 | formatter = logging.Formatter( 29 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 30 | ) 31 | 32 | # Add the formatter to the console handler 33 | console_handler.setFormatter(formatter) 34 | 35 | # Add the console handler to the logger 36 | logger.addHandler(console_handler) 37 | 38 | return logger 39 | 40 | 41 | # Example usage 42 | if __name__ == "__main__": 43 | logger = get_logger("example_logger", "DEBUG") 44 | logger.debug("This is a debug message") 45 | logger.info("This is an info message") 46 | logger.warning("This is a warning message") 47 | logger.error("This is an error message") 48 | logger.critical("This is a critical message") 49 | -------------------------------------------------------------------------------- /fastapi+svelte/main.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import io 3 | import sqlite3 4 | 5 | from logger import get_logger 6 | 7 | from fastapi import FastAPI, File, HTTPException, UploadFile 8 | from fastapi.middleware.cors import CORSMiddleware 9 | from fastapi.responses import FileResponse 10 | 11 | app = FastAPI() 12 | logger = get_logger("fastapi+svelte", "INFO") 13 | 14 | # Enable CORS 15 | app.add_middleware( 16 | CORSMiddleware, 17 | allow_origins=["http://localhost:5173"], # Add your Svelte app's URL 18 | allow_credentials=True, 19 | allow_methods=["*"], 20 | allow_headers=["*"], 21 | ) 22 | 23 | DB_NAME = "database.sqlite" 24 | 25 | 26 | @app.post("/upload") 27 | async def upload_csv(file: UploadFile = File(...)): 28 | if not file.filename.endswith(".csv"): 29 | raise HTTPException(status_code=400, detail="Only CSV files are allowed") 30 | 31 | try: 32 | content = await file.read() 33 | csv_reader = csv.reader(io.StringIO(content.decode("utf-8"))) 34 | headers = next(csv_reader) 35 | 36 | with sqlite3.connect(DB_NAME) as conn: 37 | cursor = conn.cursor() 38 | cursor.execute("DROP TABLE IF EXISTS data") 39 | cursor.execute(f"CREATE TABLE data ({', '.join(headers)})") 40 | 41 | for row in csv_reader: 42 | if len(row) == len(headers): 43 | cursor.execute( 44 | f"INSERT INTO data VALUES ({', '.join(['?' for _ in row])})", 45 | row, 46 | ) 47 | 48 | logger.info("CSV uploaded and database created successfully") 49 | return {"message": "CSV uploaded and database created successfully"} 50 | except Exception as e: 51 | logger.error(f"Error uploading CSV: {str(e)}") 52 | raise HTTPException(status_code=500, detail=f"Error uploading CSV: {str(e)}") 53 | 54 | 55 | @app.get("/data") 56 | async def get_data(): 57 | try: 58 | with sqlite3.connect(DB_NAME) as conn: 59 | cursor = conn.cursor() 60 | cursor.execute("SELECT * FROM data") 61 | data = cursor.fetchall() 62 | headers = [description[0] for description in cursor.description] 63 | return {"headers": headers, "data": data} 64 | except sqlite3.OperationalError: 65 | return {"headers": [], "data": []} 66 | except Exception as e: 67 | logger.error(f"Error fetching data: {str(e)}") 68 | return {"headers": [], "data": [], "error": str(e)} 69 | 70 | 71 | @app.put("/update/{row_id}") 72 | async def update_row(row_id: int, updated_data: dict): 73 | conn = sqlite3.connect(DB_NAME) 74 | cursor = conn.cursor() 75 | 76 | set_clause = ", ".join([f"{key} = ?" for key in updated_data.keys()]) 77 | values = list(updated_data.values()) 78 | values.append(row_id) 79 | 80 | cursor.execute(f"UPDATE data SET {set_clause} WHERE rowid = ?", values) 81 | conn.commit() 82 | conn.close() 83 | 84 | return {"message": f"Row {row_id} updated successfully"} 85 | 86 | 87 | @app.delete("/delete/{row_id}") 88 | async def delete_row(row_id: int): 89 | conn = sqlite3.connect(DB_NAME) 90 | cursor = conn.cursor() 91 | cursor.execute("DELETE FROM data WHERE rowid = ?", (row_id,)) 92 | conn.commit() 93 | conn.close() 94 | return {"message": f"Row {row_id} deleted successfully"} 95 | 96 | 97 | @app.get("/download") 98 | async def download_csv(): 99 | conn = sqlite3.connect(DB_NAME) 100 | cursor = conn.cursor() 101 | cursor.execute("SELECT * FROM data") 102 | data = cursor.fetchall() 103 | headers = [description[0] for description in cursor.description] 104 | conn.close() 105 | 106 | output = io.StringIO() 107 | writer = csv.writer(output) 108 | writer.writerow(headers) 109 | writer.writerows(data) 110 | 111 | output.seek(0) 112 | 113 | return FileResponse( 114 | io.BytesIO(output.getvalue().encode()), 115 | media_type="text/csv", 116 | filename="data.csv", 117 | ) 118 | -------------------------------------------------------------------------------- /fastapi+svelte/svelte-app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | 9 | # OS 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # Env 14 | .env 15 | .env.* 16 | !.env.example 17 | !.env.test 18 | 19 | # Vite 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | -------------------------------------------------------------------------------- /fastapi+svelte/svelte-app/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /fastapi+svelte/svelte-app/.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /fastapi+svelte/svelte-app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 8 | } 9 | -------------------------------------------------------------------------------- /fastapi+svelte/svelte-app/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /fastapi+svelte/svelte-app/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import ts from 'typescript-eslint'; 3 | import svelte from 'eslint-plugin-svelte'; 4 | import prettier from 'eslint-config-prettier'; 5 | import globals from 'globals'; 6 | 7 | /** @type {import('eslint').Linter.Config[]} */ 8 | export default [ 9 | js.configs.recommended, 10 | ...ts.configs.recommended, 11 | ...svelte.configs['flat/recommended'], 12 | prettier, 13 | ...svelte.configs['flat/prettier'], 14 | { 15 | languageOptions: { 16 | globals: { 17 | ...globals.browser, 18 | ...globals.node 19 | } 20 | } 21 | }, 22 | { 23 | files: ['**/*.svelte'], 24 | languageOptions: { 25 | parserOptions: { 26 | parser: ts.parser 27 | } 28 | } 29 | }, 30 | { 31 | ignores: ['build/', '.svelte-kit/', 'dist/'] 32 | } 33 | ]; 34 | -------------------------------------------------------------------------------- /fastapi+svelte/svelte-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --check . && eslint .", 12 | "format": "prettier --write ." 13 | }, 14 | "devDependencies": { 15 | "@sveltejs/adapter-auto": "^3.0.0", 16 | "@sveltejs/kit": "^2.0.0", 17 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 18 | "@types/eslint": "^9.6.0", 19 | "eslint": "^9.0.0", 20 | "eslint-config-prettier": "^9.1.0", 21 | "eslint-plugin-svelte": "^2.36.0", 22 | "globals": "^15.0.0", 23 | "prettier": "^3.1.1", 24 | "prettier-plugin-svelte": "^3.1.2", 25 | "svelte": "^4.2.7", 26 | "svelte-check": "^4.0.0", 27 | "typescript": "^5.0.0", 28 | "typescript-eslint": "^8.0.0", 29 | "vite": "^5.0.3" 30 | }, 31 | "type": "module" 32 | } 33 | -------------------------------------------------------------------------------- /fastapi+svelte/svelte-app/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /fastapi+svelte/svelte-app/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /fastapi+svelte/svelte-app/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 86 | 87 |
88 |

Look at Your Data

89 | 90 |
91 |
92 | 93 | 94 |
95 | {#if tableData.length > 0} 96 | 97 | {/if} 98 |
99 | 100 | {#if message} 101 |

{message}

102 | {/if} 103 | 104 | {#if tableData.length > 0} 105 |
106 | 107 | 108 | 109 | {#each headers as header} 110 | 111 | {/each} 112 | 113 | 114 | 115 | 116 | {#each tableData as row, rowIndex} 117 | 118 | {#each row as cell, colIndex} 119 | 133 | {/each} 134 | 137 | 138 | {/each} 139 | 140 |
{header}Actions
120 |
startEditing(rowIndex, colIndex, cell)} 126 | on:input={(e) => editedValue = e.target.textContent} 127 | on:keydown={(e) => handleKeyDown(e, row[0])} 128 | on:blur={() => saveEdit(row[0])} 129 | > 130 | {cell} 131 |
132 |
135 | 136 |
141 |
142 | {:else} 143 |
144 | {/if} 145 |
146 | 147 | 218 | -------------------------------------------------------------------------------- /fastapi+svelte/svelte-app/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter() 15 | } 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /fastapi+svelte/svelte-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /fastapi+svelte/svelte-app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | server: { 7 | proxy: { 8 | '/api': { 9 | target: 'http://localhost:8000', 10 | changeOrigin: true, 11 | rewrite: (path) => path.replace(/^\/api/, '') 12 | } 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /fastapi/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | 5 | def get_logger(name: str = __name__, level: str = "INFO") -> logging.Logger: 6 | """ 7 | Initialize a simple logger that outputs to the console. 8 | 9 | Args: 10 | name (str): The name of the logger. Defaults to the module name. 11 | level (str): The logging level. Defaults to "INFO". 12 | 13 | Returns: 14 | logging.Logger: A configured logger instance. 15 | """ 16 | # Create a logger 17 | logger = logging.getLogger(name) 18 | 19 | # Set the logging level 20 | level = getattr(logging, level.upper(), logging.INFO) 21 | logger.setLevel(level) 22 | 23 | # Create a console handler and set its level 24 | console_handler = logging.StreamHandler(sys.stdout) 25 | console_handler.setLevel(level) 26 | 27 | # Create a formatter 28 | formatter = logging.Formatter( 29 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 30 | ) 31 | 32 | # Add the formatter to the console handler 33 | console_handler.setFormatter(formatter) 34 | 35 | # Add the console handler to the logger 36 | logger.addHandler(console_handler) 37 | 38 | return logger 39 | 40 | 41 | # Example usage 42 | if __name__ == "__main__": 43 | logger = get_logger("example_logger", "DEBUG") 44 | logger.debug("This is a debug message") 45 | logger.info("This is an info message") 46 | logger.warning("This is a warning message") 47 | logger.error("This is an error message") 48 | logger.critical("This is a critical message") 49 | -------------------------------------------------------------------------------- /fastapi/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | uv run uvicorn main:app --reload 3 | """ 4 | 5 | import csv 6 | import io 7 | import sqlite3 8 | 9 | from logger import get_logger 10 | 11 | from fastapi import FastAPI, File, Request, UploadFile 12 | from fastapi.exceptions import HTTPException 13 | from fastapi.responses import StreamingResponse 14 | from fastapi.staticfiles import StaticFiles 15 | from fastapi.templating import Jinja2Templates 16 | 17 | app = FastAPI() 18 | logger = get_logger("fastapi", "INFO") 19 | 20 | app.mount("/static", StaticFiles(directory="static"), name="static") 21 | templates = Jinja2Templates(directory="templates") 22 | 23 | 24 | # Database setup 25 | def init_db(): 26 | conn = sqlite3.connect("database.db") 27 | conn.close() 28 | 29 | 30 | init_db() 31 | 32 | 33 | # Remove the following function as it's not necessary for the core functionality 34 | @app.get("/") 35 | async def home(request: Request): 36 | return templates.TemplateResponse("index.html", {"request": request}) 37 | 38 | 39 | @app.post("/upload") 40 | async def upload_csv(file: UploadFile = File(...)): 41 | content = await file.read() 42 | decoded_content = content.decode("utf-8").splitlines() 43 | csv_reader = csv.DictReader(decoded_content) 44 | 45 | conn = sqlite3.connect("database.db") 46 | cursor = conn.cursor() 47 | 48 | # Get column names from the CSV 49 | columns = csv_reader.fieldnames 50 | 51 | if not columns: 52 | return {"message": "Error: CSV file is empty or has no headers"} 53 | 54 | # Create table based on CSV columns 55 | create_table_sql = f"""CREATE TABLE IF NOT EXISTS data ( 56 | {', '.join([f'{col} TEXT' for col in columns])} 57 | )""" 58 | cursor.execute(create_table_sql) 59 | 60 | # Insert data 61 | placeholders = ", ".join(["?" for _ in columns]) 62 | insert_sql = f"INSERT INTO data ({', '.join(columns)}) VALUES ({placeholders})" 63 | 64 | for row in csv_reader: 65 | cursor.execute(insert_sql, [row[col] for col in columns]) 66 | 67 | conn.commit() 68 | conn.close() 69 | 70 | return {"message": "CSV uploaded and database initialized"} 71 | 72 | 73 | @app.get("/data") 74 | async def get_data(): 75 | conn = sqlite3.connect("database.db") 76 | cursor = conn.cursor() 77 | 78 | # Get column names 79 | cursor.execute("PRAGMA table_info(data)") 80 | columns = [column[1] for column in cursor.fetchall()] 81 | 82 | # Get data 83 | cursor.execute("SELECT * FROM data") 84 | data = cursor.fetchall() 85 | 86 | conn.close() 87 | logger.debug("Columns: %r, Data: %r", columns, data[0]) 88 | return {"columns": columns, "data": data} 89 | 90 | 91 | @app.post("/update") 92 | async def update_data(request: Request): 93 | form_data = await request.form() 94 | logger.info("Received form data: %r", form_data) 95 | 96 | id = form_data.get("id") 97 | if not id: 98 | raise HTTPException(status_code=400, detail="ID is required") 99 | 100 | conn = sqlite3.connect("database.db") 101 | cursor = conn.cursor() 102 | 103 | # Get column names 104 | cursor.execute("PRAGMA table_info(data)") 105 | valid_columns = set(column[1] for column in cursor.fetchall()) 106 | 107 | # Prepare the update query 108 | update_columns = [] 109 | update_values = [] 110 | for key, value in form_data.items(): 111 | if key != "id" and key in valid_columns: 112 | update_columns.append(f"{key} = ?") 113 | update_values.append(value) 114 | elif key != "id": 115 | logger.warning("Ignoring invalid column: %r", key) 116 | 117 | if not update_columns: 118 | raise HTTPException(status_code=400, detail="No valid columns to update") 119 | 120 | update_query = f"UPDATE data SET {', '.join(update_columns)} WHERE id = ?" 121 | update_values.append(id) 122 | 123 | logger.debug("Update query: %r", update_query) 124 | logger.debug("Update values: %r", update_values) 125 | 126 | try: 127 | cursor.execute(update_query, update_values) 128 | conn.commit() 129 | logger.info("Updated row with id: %r", id) 130 | return {"message": "Data updated successfully"} 131 | except sqlite3.Error as e: 132 | logger.error("Error updating data: %r", e) 133 | raise HTTPException(status_code=500, detail="Error updating data") from e 134 | finally: 135 | conn.close() 136 | 137 | 138 | @app.post("/delete") 139 | async def delete_data(request: Request): 140 | form_data = await request.json() 141 | logger.info("Received delete request: %r", form_data) 142 | 143 | id = form_data.get("id") 144 | if not id: 145 | raise HTTPException(status_code=400, detail="ID is required") 146 | 147 | conn = sqlite3.connect("database.db") 148 | cursor = conn.cursor() 149 | 150 | try: 151 | cursor.execute("DELETE FROM data WHERE id = ?", (id,)) 152 | conn.commit() 153 | logger.info("Deleted row with id: %r", id) 154 | return {"message": "Data deleted successfully"} 155 | except sqlite3.Error as e: 156 | logger.error("Error deleting data: %r", e) 157 | raise HTTPException(status_code=500, detail="Error deleting data") from e 158 | finally: 159 | conn.close() 160 | 161 | 162 | @app.get("/download") 163 | async def download_csv(): 164 | conn = sqlite3.connect("database.db") 165 | cursor = conn.cursor() 166 | 167 | # Get column names 168 | cursor.execute("PRAGMA table_info(data)") 169 | columns = [column[1] for column in cursor.fetchall()] 170 | 171 | # Get data 172 | cursor.execute("SELECT * FROM data") 173 | data = cursor.fetchall() 174 | 175 | conn.close() 176 | 177 | # Create CSV string 178 | output = io.StringIO() 179 | writer = csv.writer(output) 180 | writer.writerow(columns) 181 | writer.writerows(data) 182 | 183 | # Create a streaming response 184 | response = StreamingResponse(iter([output.getvalue()]), media_type="text/csv") 185 | response.headers["Content-Disposition"] = "attachment; filename=database_export.csv" 186 | 187 | return response 188 | -------------------------------------------------------------------------------- /fastapi/static/script.js: -------------------------------------------------------------------------------- 1 | let DEBUG = true; 2 | 3 | // Custom debug logging function 4 | function debugLog(...args) { 5 | if (DEBUG) { 6 | console.log(...args); 7 | } 8 | } 9 | 10 | async function uploadCSV() { 11 | const fileInput = document.getElementById('csvFile'); 12 | const file = fileInput.files[0]; 13 | if (!file) { 14 | alert('Please select a CSV file'); 15 | return; 16 | } 17 | 18 | const formData = new FormData(); 19 | formData.append('file', file); 20 | 21 | try { 22 | const response = await axios.post('/upload', formData); 23 | alert(response.data.message); 24 | loadData(); 25 | } catch (error) { 26 | console.error('Error uploading CSV:', error); 27 | alert('Error uploading CSV'); 28 | } 29 | } 30 | 31 | function getColumnClass(value) { 32 | if (value.length <= 5) { 33 | return 'col-narrow'; 34 | } else if (value.length <= 200) { 35 | return 'col-medium'; 36 | } else { 37 | return 'col-wide'; 38 | } 39 | } 40 | 41 | async function loadData() { 42 | try { 43 | const response = await axios.get('/data'); 44 | debugLog('Received data from server:', response.data); 45 | const { columns, data } = response.data; 46 | const table = document.getElementById('dataTable'); 47 | const thead = table.querySelector('thead'); 48 | const tbody = table.querySelector('tbody'); 49 | 50 | // Clear existing content 51 | thead.innerHTML = ''; 52 | tbody.innerHTML = ''; 53 | 54 | // Create header row 55 | const headerRow = document.createElement('tr'); 56 | columns.forEach(col => { 57 | const th = document.createElement('th'); 58 | th.textContent = col; 59 | headerRow.appendChild(th); 60 | }); 61 | headerRow.innerHTML += 'Actions'; 62 | thead.appendChild(headerRow); 63 | 64 | // Create data rows 65 | data.forEach(row => { 66 | const tr = document.createElement('tr'); 67 | columns.forEach((col, index) => { 68 | const td = document.createElement('td'); 69 | const textarea = document.createElement('textarea'); 70 | textarea.value = row[index]; 71 | textarea.setAttribute('data-id', row[0]); 72 | textarea.setAttribute('data-field', col); 73 | td.appendChild(textarea); 74 | td.className = getColumnClass(row[index]); 75 | tr.appendChild(td); 76 | }); 77 | const actionTd = document.createElement('td'); 78 | const updateButton = document.createElement('button'); 79 | updateButton.textContent = 'Update'; 80 | updateButton.onclick = () => updateRow(updateButton); 81 | updateButton.setAttribute('data-id', row[0]); 82 | actionTd.appendChild(updateButton); 83 | const deleteButton = document.createElement('button'); 84 | deleteButton.textContent = 'Delete'; 85 | deleteButton.onclick = () => deleteRow(deleteButton); 86 | deleteButton.setAttribute('data-id', row[0]); 87 | actionTd.appendChild(deleteButton); 88 | tr.appendChild(actionTd); 89 | tbody.appendChild(tr); 90 | }); 91 | } catch (error) { 92 | console.error('Error loading data:', error); 93 | alert('Error loading data'); 94 | } 95 | } 96 | 97 | // Update the updateRow function to use data-id 98 | async function updateRow(button) { 99 | const id = button.getAttribute('data-id'); 100 | console.log('Updating row with id:', id); 101 | const formData = new FormData(); 102 | formData.append('id', id); 103 | 104 | const updatedData = {}; 105 | const row = button.closest('tr'); 106 | const textareas = row.querySelectorAll('textarea'); 107 | textareas.forEach(textarea => { 108 | const field = textarea.getAttribute('data-field'); 109 | const value = textarea.value; 110 | console.log(`Field: ${field}, Value: ${value}`); 111 | formData.append(field, value); 112 | updatedData[field] = value; 113 | }); 114 | 115 | console.log('Sending data:', updatedData); 116 | 117 | try { 118 | const response = await axios.post('/update', formData); 119 | debugLog('Server response:', response.data); 120 | alert(response.data.message); 121 | await loadData(); 122 | } catch (error) { 123 | console.error('Error updating data:', error.response?.data || error.message); 124 | alert('Error updating data'); 125 | } 126 | } 127 | 128 | async function deleteRow(button) { 129 | const id = button.getAttribute('data-id'); 130 | console.log('Deleting row with id:', id); 131 | 132 | if (!confirm('Are you sure you want to delete this row?')) { 133 | return; 134 | } 135 | 136 | try { 137 | const response = await axios.post('/delete', { id: id }); 138 | debugLog('Server response:', response.data); 139 | alert(response.data.message); 140 | await loadData(); 141 | } catch (error) { 142 | console.error('Error deleting data:', error.response?.data || error.message); 143 | alert('Error deleting data'); 144 | } 145 | } 146 | 147 | async function downloadCSV() { 148 | try { 149 | const response = await axios.get('/download', { responseType: 'blob' }); 150 | const blob = new Blob([response.data], { type: 'text/csv' }); 151 | const url = window.URL.createObjectURL(blob); 152 | const a = document.createElement('a'); 153 | a.style.display = 'none'; 154 | a.href = url; 155 | a.download = 'database_export.csv'; 156 | document.body.appendChild(a); 157 | a.click(); 158 | window.URL.revokeObjectURL(url); 159 | } catch (error) { 160 | console.error('Error downloading CSV:', error); 161 | alert('Error downloading CSV'); 162 | } 163 | } 164 | 165 | document.addEventListener('DOMContentLoaded', loadData); 166 | -------------------------------------------------------------------------------- /fastapi/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .container { 8 | width: 100%; 9 | max-width: none; 10 | padding: 0 2em; 11 | box-sizing: border-box; 12 | } 13 | 14 | table { 15 | width: 100%; 16 | border-collapse: collapse; 17 | margin-top: 20px; 18 | } 19 | 20 | th, td { 21 | border: 1px solid #ddd; 22 | padding: 8px; 23 | text-align: left; 24 | word-wrap: break-word; 25 | overflow-wrap: break-word; 26 | } 27 | 28 | th { 29 | background-color: #f2f2f2; 30 | position: sticky; 31 | top: 0; 32 | z-index: 10; 33 | } 34 | 35 | input[type="file"], button { 36 | margin-top: 10px; 37 | } 38 | 39 | .col-narrow { 40 | width: 2em; 41 | max-width: 2em; 42 | } 43 | 44 | .col-medium { 45 | width: 20%; 46 | max-width: 20%; 47 | } 48 | 49 | .col-wide { 50 | width: 70%; 51 | max-width: 70%; 52 | } 53 | 54 | td { 55 | height: 10em; 56 | vertical-align: top; 57 | } 58 | 59 | textarea { 60 | width: 100%; 61 | height: 100%; 62 | box-sizing: border-box; 63 | resize: both; 64 | border: none; 65 | background: transparent; 66 | font-family: inherit; 67 | font-size: inherit; 68 | padding: 0; 69 | } 70 | -------------------------------------------------------------------------------- /fastapi/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Look at Your Data 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Look at Your Data

14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 |
24 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /fasthtml/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | 5 | def get_logger(name: str = __name__, level: str = "INFO") -> logging.Logger: 6 | """ 7 | Initialize a simple logger that outputs to the console. 8 | 9 | Args: 10 | name (str): The name of the logger. Defaults to the module name. 11 | level (str): The logging level. Defaults to "INFO". 12 | 13 | Returns: 14 | logging.Logger: A configured logger instance. 15 | """ 16 | # Create a logger 17 | logger = logging.getLogger(name) 18 | 19 | # Set the logging level 20 | level = getattr(logging, level.upper(), logging.INFO) 21 | logger.setLevel(level) 22 | 23 | # Create a console handler and set its level 24 | console_handler = logging.StreamHandler(sys.stdout) 25 | console_handler.setLevel(level) 26 | 27 | # Create a formatter 28 | formatter = logging.Formatter( 29 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 30 | ) 31 | 32 | # Add the formatter to the console handler 33 | console_handler.setFormatter(formatter) 34 | 35 | # Add the console handler to the logger 36 | logger.addHandler(console_handler) 37 | 38 | return logger 39 | 40 | 41 | # Example usage 42 | if __name__ == "__main__": 43 | logger = get_logger("example_logger", "DEBUG") 44 | logger.debug("This is a debug message") 45 | logger.info("This is an info message") 46 | logger.warning("This is a warning message") 47 | logger.error("This is an error message") 48 | logger.critical("This is a critical message") 49 | -------------------------------------------------------------------------------- /fasthtml/main-jeremy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Source: https://gist.github.com/jph00/0590da374a11b8def808c1821abdd42a 3 | """ 4 | from fasthtml.common import * 5 | 6 | db = database(':memory:') 7 | tbl = None 8 | hdrs = (Style(''' 9 | button,input { margin: 0 1rem; } 10 | [role="group"] { border: 1px solid #ccc; } 11 | '''), ) 12 | app, rt = fast_app(live=True, hdrs=hdrs) 13 | 14 | @rt("/") 15 | async def get(): 16 | return Titled("CSV Uploader", 17 | Group( 18 | Input(type="file", name="csv_file", accept=".csv"), 19 | Button("Upload", hx_post="/upload", hx_target="#results", 20 | hx_encoding="multipart/form-data", hx_include='previous input'), 21 | A('Download', href='/download', type="button") 22 | ), 23 | Div(id="results")) 24 | 25 | def render_row(row): 26 | vals = [Td(Input(value=v, name=k)) for k,v in row.items()] 27 | vals.append(Td(Group(Button('delete', hx_get=remove.rt(id=row['id'])), 28 | Button('update', hx_post='/update', hx_include="closest tr")))) 29 | return Tr(*vals, hx_target='closest tr', hx_swap='outerHTML') 30 | 31 | @rt 32 | async def download(): 33 | csv_data = [",".join(map(str, tbl.columns_dict))] 34 | csv_data += [",".join(map(str, row.values())) for row in tbl()] 35 | headers = {'Content-Disposition': 'attachment; filename="data.csv"'} 36 | return Response("\n".join(csv_data), media_type="text/csv", headers=headers) 37 | 38 | @rt('/update') 39 | def post(d:dict): return render_row(tbl.update(d)) 40 | 41 | @rt 42 | def remove(id:int): tbl.delete(id) 43 | 44 | @rt("/upload") 45 | async def post(csv_file: UploadFile): 46 | global tbl 47 | if not csv_file.filename.endswith('.csv'): return "Please upload a CSV file" 48 | tbl = db.import_file('test', await csv_file.read(), pk='id') 49 | header = Tr(*map(Th, tbl.columns_dict)) 50 | vals = [render_row(row) for row in tbl()] 51 | return Table(Thead(header), Tbody(*vals)) 52 | 53 | serve() -------------------------------------------------------------------------------- /fasthtml/main.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import io 3 | import sqlite3 4 | 5 | from logger import get_logger 6 | 7 | from fastapi.responses import StreamingResponse 8 | from fasthtml.common import * 9 | 10 | app, rt = fast_app() 11 | logger = get_logger("fasthtml", "INFO") 12 | 13 | 14 | # Database setup 15 | def init_db(): 16 | conn = sqlite3.connect("database.db") 17 | conn.close() 18 | 19 | 20 | init_db() 21 | 22 | 23 | @rt("/") 24 | def index(): 25 | inp = Input( 26 | type="file", 27 | name="csv_file", 28 | accept=".csv", 29 | multiple=False, 30 | required=True, 31 | ) 32 | upload_button = Button("Upload CSV", cls="btn") 33 | download_button = A("Download CSV", href="/download", cls="btn download-btn") 34 | 35 | add = Form( 36 | Group(inp, upload_button, download_button), 37 | hx_post="/upload", 38 | hx_target="#data-table", 39 | hx_swap="outerHTML", 40 | enctype="multipart/form-data", 41 | ) 42 | 43 | # Check if there's data in the database 44 | conn = sqlite3.connect("database.db") 45 | cursor = conn.cursor() 46 | cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='data'") 47 | table_exists = cursor.fetchone() is not None 48 | 49 | if table_exists: 50 | cursor.execute("SELECT COUNT(*) FROM data") 51 | row_count = cursor.fetchone()[0] 52 | if row_count > 0: 53 | data_table = get_data() 54 | else: 55 | data_table = P("No data available. Please upload a CSV file.") 56 | else: 57 | data_table = P("No data available. Please upload a CSV file.") 58 | 59 | conn.close() 60 | 61 | return Title("Look at Your Data"), Main( 62 | Link(rel="stylesheet", href="/static/style.css"), 63 | H1("Look at Your Data"), 64 | add, 65 | Div(data_table, id="data-table", cls="table-container", style="width: 100%;"), 66 | cls="container-fluid", 67 | ) 68 | 69 | 70 | @rt("/upload") 71 | async def upload_csv(csv_file: UploadFile): 72 | contents = await csv_file.read() 73 | decoded_content = contents.decode("utf-8").splitlines() 74 | csv_reader = csv.DictReader(decoded_content) 75 | 76 | conn = sqlite3.connect("database.db") 77 | cursor = conn.cursor() 78 | 79 | # Get column names from the CSV 80 | columns = csv_reader.fieldnames 81 | 82 | if not columns: 83 | return P("Error: CSV file is empty or has no headers") 84 | 85 | # Create table based on CSV columns 86 | create_table_sql = f"""CREATE TABLE IF NOT EXISTS data ( 87 | {', '.join([f'{col} TEXT' for col in columns])} 88 | )""" 89 | cursor.execute(create_table_sql) 90 | 91 | # Insert data 92 | placeholders = ", ".join(["?" for _ in columns]) 93 | insert_sql = f"INSERT INTO data ({', '.join(columns)}) VALUES ({placeholders})" 94 | 95 | for row in csv_reader: 96 | cursor.execute(insert_sql, [row[col] for col in columns]) 97 | 98 | conn.commit() 99 | conn.close() 100 | 101 | return get_data() 102 | 103 | 104 | def create_table(columns, data): 105 | table = Table( 106 | Thead( 107 | Tr( 108 | *[Th(col, cls=f"col-{getColumnClass(col)}") for col in columns] 109 | + [Th("Actions")] 110 | ) 111 | ), 112 | Tbody( 113 | *[ 114 | Tr( 115 | *[ 116 | Td(str(row[i]), cls=f"col-{getColumnClass(str(row[i]))}") 117 | if i == 0 118 | else Td( 119 | Textarea( 120 | str(row[i]), 121 | name=f"{columns[i]}_{row[0]}", 122 | cls=f"textarea-{getColumnClass(str(row[i]))}", 123 | ), 124 | cls=f"col-{getColumnClass(str(row[i]))}", 125 | ) 126 | for i in range(len(columns)) 127 | ] 128 | + [ 129 | Td( 130 | Button( 131 | "Update", 132 | hx_post=f"/update/{row[0]}", 133 | hx_include="closest tr", 134 | hx_target="closest tr", 135 | cls="btn", 136 | ), 137 | Button( 138 | "Delete", 139 | hx_delete=f"/delete/{row[0]}", 140 | hx_target="#data-table", 141 | cls="btn", 142 | ), 143 | ) 144 | ] 145 | ) 146 | for row in data 147 | ] 148 | ), 149 | cls="table", 150 | ) 151 | return Div(table, cls="table-container") 152 | 153 | 154 | def getColumnClass(value): 155 | if len(value) <= 5: 156 | return "narrow" 157 | elif len(value) <= 200: 158 | return "medium" 159 | else: 160 | return "wide" 161 | 162 | 163 | @rt("/data") 164 | def get_data(): 165 | conn = sqlite3.connect("database.db") 166 | cursor = conn.cursor() 167 | 168 | # Get column names 169 | cursor.execute("PRAGMA table_info(data)") 170 | columns = [column[1] for column in cursor.fetchall()] 171 | logger.info(f"Columns: {columns}") 172 | 173 | # Get data 174 | cursor.execute("SELECT * FROM data") 175 | data = cursor.fetchall() 176 | logger.info(f"Data: {data[0]}") 177 | conn.close() 178 | 179 | table = create_table(columns, data) 180 | 181 | return table 182 | 183 | 184 | @rt("/update/{id}", methods=["POST"]) 185 | def update(id: int, form_data: dict): 186 | conn = sqlite3.connect("database.db") 187 | cursor = conn.cursor() 188 | 189 | # Get column names 190 | cursor.execute("PRAGMA table_info(data)") 191 | columns = [column[1] for column in cursor.fetchall()] 192 | 193 | # Filter out the 'id' column and prepare the update data 194 | update_data = {col: form_data.get(f"{col}_{id}") for col in columns if col != "id"} 195 | 196 | # Prepare the SQL update statement 197 | update_sql = f"UPDATE data SET {', '.join([f'{col}=?' for col in update_data.keys()])} WHERE id=?" 198 | 199 | # Execute the update 200 | cursor.execute(update_sql, (*update_data.values(), id)) 201 | conn.commit() 202 | 203 | # Fetch the updated row 204 | cursor.execute("SELECT * FROM data WHERE id=?", (id,)) 205 | updated_row = cursor.fetchone() 206 | conn.close() 207 | 208 | # Create and return the updated table row contents 209 | return [ 210 | Td(str(updated_row[i]), cls=f"col-{getColumnClass(str(updated_row[i]))}") 211 | if i == 0 212 | else Td( 213 | Textarea( 214 | str(updated_row[i]), 215 | name=f"{columns[i]}_{updated_row[0]}", 216 | cls=f"textarea-{getColumnClass(str(updated_row[i]))}", 217 | ), 218 | cls=f"col-{getColumnClass(str(updated_row[i]))}", 219 | ) 220 | for i in range(len(columns)) 221 | ] + [ 222 | Td( 223 | Button( 224 | "Update", 225 | hx_post=f"/update/{updated_row[0]}", 226 | hx_include="closest tr", 227 | hx_target="closest tr", 228 | cls="btn", 229 | ), 230 | Button( 231 | "Delete", 232 | hx_delete=f"/delete/{updated_row[0]}", 233 | hx_target="#data-table", 234 | cls="btn", 235 | ), 236 | ) 237 | ] 238 | 239 | 240 | @rt("/delete/{id}", methods=["DELETE"]) 241 | def delete(id: int): 242 | conn = sqlite3.connect("database.db") 243 | cursor = conn.cursor() 244 | cursor.execute("DELETE FROM data WHERE id=?", (id,)) 245 | conn.commit() 246 | conn.close() 247 | return get_data() 248 | 249 | 250 | @rt("/download") 251 | def download_csv(): 252 | conn = sqlite3.connect("database.db") 253 | cursor = conn.cursor() 254 | 255 | # Get column names 256 | cursor.execute("PRAGMA table_info(data)") 257 | columns = [column[1] for column in cursor.fetchall()] 258 | 259 | # Fetch all data 260 | cursor.execute("SELECT * FROM data") 261 | data = cursor.fetchall() 262 | 263 | conn.close() 264 | 265 | # Create a CSV string 266 | output = io.StringIO() 267 | writer = csv.writer(output) 268 | 269 | # Write header 270 | writer.writerow(columns) 271 | 272 | # Write data 273 | writer.writerows(data) 274 | 275 | # Create a StreamingResponse 276 | response = StreamingResponse(iter([output.getvalue()]), media_type="text/csv") 277 | response.headers["Content-Disposition"] = "attachment; filename=data.csv" 278 | 279 | return response 280 | 281 | 282 | serve() 283 | -------------------------------------------------------------------------------- /fasthtml/static/style.css: -------------------------------------------------------------------------------- 1 | .col-narrow { 2 | width: 5%; 3 | min-width: 5em; 4 | max-width: 10em; 5 | } 6 | 7 | .col-medium { 8 | width: 20%; 9 | max-width: 20%; 10 | } 11 | 12 | .col-wide { 13 | width: 70%; 14 | max-width: 70%; 15 | } 16 | 17 | .col-narrow textarea, .col-medium textarea, .col-wide textarea { 18 | width: 100%; 19 | height: 10em; 20 | padding: 5px; 21 | box-sizing: border-box; 22 | background: transparent; 23 | resize: vertical; 24 | overflow-y: auto; 25 | font-family: inherit; 26 | font-size: inherit; 27 | } 28 | 29 | .col-narrow, .col-medium, .col-wide { 30 | padding: 5px; 31 | } 32 | 33 | .table-container { 34 | overflow-x: auto; 35 | } 36 | 37 | .table { 38 | width: 100%; 39 | border-collapse: collapse; 40 | } 41 | 42 | .btn { 43 | display: inline-block; 44 | padding: 10px 20px; 45 | margin: 0 5px; 46 | background-color: #0275d8; 47 | color: white; 48 | text-decoration: none; 49 | border-radius: 5px; 50 | border: none; 51 | cursor: pointer; 52 | font-size: 1em; 53 | text-align: center; 54 | } 55 | 56 | form { 57 | display: flex; 58 | align-items: center; 59 | margin-bottom: 20px; 60 | } 61 | 62 | input[type="file"] { 63 | margin-right: 10px; 64 | } -------------------------------------------------------------------------------- /nextjs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /nextjs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /nextjs/app/api/data/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { db } from '../../../lib/db'; 3 | 4 | export async function GET() { 5 | try { 6 | const data = db.prepare('SELECT * FROM data').all(); 7 | return NextResponse.json(data); 8 | } catch (error) { 9 | console.error('Error fetching data:', error); 10 | return NextResponse.json({ error: 'Error fetching data' }, { status: 500 }); 11 | } 12 | } 13 | 14 | export async function PUT(req: NextRequest) { 15 | const { id, column, value } = await req.json(); 16 | 17 | try { 18 | // Start a transaction 19 | db.prepare('BEGIN').run(); 20 | 21 | // Update the data 22 | const updateStmt = db.prepare(`UPDATE data SET ${column} = ? WHERE id = ?`); 23 | const result = updateStmt.run(value, id); 24 | 25 | if (result.changes === 0) { 26 | // If no rows were updated, rollback and return an error 27 | db.prepare('ROLLBACK').run(); 28 | return NextResponse.json({ error: 'No rows updated' }, { status: 404 }); 29 | } 30 | 31 | // Commit the transaction 32 | db.prepare('COMMIT').run(); 33 | 34 | return NextResponse.json({ message: 'Data updated successfully' }); 35 | } catch (error) { 36 | // Rollback the transaction in case of an error 37 | db.prepare('ROLLBACK').run(); 38 | console.error('Error updating data:', error); 39 | return NextResponse.json({ error: 'Error updating data' }, { status: 500 }); 40 | } 41 | } 42 | 43 | export async function DELETE(req: NextRequest) { 44 | const { searchParams } = new URL(req.url); 45 | const id = searchParams.get('id'); 46 | 47 | if (!id) { 48 | return NextResponse.json({ error: 'Missing id parameter' }, { status: 400 }); 49 | } 50 | 51 | try { 52 | db.prepare('DELETE FROM data WHERE id = ?').run(id); 53 | return NextResponse.json({ message: 'Row deleted successfully' }); 54 | } catch (error) { 55 | console.error('Error deleting row:', error); 56 | return NextResponse.json({ error: 'Error deleting row' }, { status: 500 }); 57 | } 58 | } -------------------------------------------------------------------------------- /nextjs/app/api/download/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { db } from '../../../lib/db'; 3 | import { stringify } from 'csv-stringify/sync'; 4 | 5 | export async function GET() { 6 | try { 7 | const data = db.prepare('SELECT * FROM data').all(); 8 | const csv = stringify(data, { header: true }); 9 | 10 | return new NextResponse(csv, { 11 | status: 200, 12 | headers: { 13 | 'Content-Type': 'text/csv', 14 | 'Content-Disposition': 'attachment; filename=data.csv', 15 | }, 16 | }); 17 | } catch (error) { 18 | console.error('Error generating CSV:', error); 19 | return NextResponse.json({ error: 'Error generating CSV' }, { status: 500 }); 20 | } 21 | } -------------------------------------------------------------------------------- /nextjs/app/api/upload/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { promises as fs } from 'fs'; 3 | import { parse } from 'csv-parse/sync'; 4 | import { db } from '../../../lib/db'; 5 | 6 | export async function POST(req: NextRequest) { 7 | const formData = await req.formData(); 8 | const file = formData.get('file') as File; 9 | 10 | if (!file) { 11 | return NextResponse.json({ error: 'No file uploaded' }, { status: 400 }); 12 | } 13 | 14 | const buffer = Buffer.from(await file.arrayBuffer()); 15 | const content = buffer.toString(); 16 | 17 | try { 18 | const records = parse(content, { columns: true, skip_empty_lines: true }); 19 | 20 | // Create table if it doesn't exist 21 | const columns = Object.keys(records[0]).map((col) => `${col} ${col.toLowerCase() === 'id' ? 'INTEGER PRIMARY KEY' : 'TEXT'}`).join(', '); 22 | await db.exec(`CREATE TABLE IF NOT EXISTS data (${columns})`); 23 | 24 | // Insert data 25 | const columnNames = Object.keys(records[0]); 26 | const stmt = db.prepare(`INSERT INTO data (${columnNames.join(', ')}) VALUES (${columnNames.map(() => '?').join(', ')})`); 27 | records.forEach((record: any) => { 28 | stmt.run(Object.values(record)); 29 | }); 30 | 31 | return NextResponse.json({ message: 'File uploaded and processed successfully' }); 32 | } catch (error) { 33 | console.error('Error processing CSV:', error); 34 | return NextResponse.json({ error: 'Error processing CSV' }, { status: 500 }); 35 | } 36 | } -------------------------------------------------------------------------------- /nextjs/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /nextjs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Look at Your Data", 9 | }; 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: Readonly<{ 14 | children: React.ReactNode; 15 | }>) { 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /nextjs/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import FileUpload from '../components/FileUpload'; 5 | import DataTable from '../components/DataTable'; 6 | import DownloadButton from '../components/DownloadButton'; 7 | 8 | export default function Home() { 9 | const [refreshKey, setRefreshKey] = useState(0); 10 | 11 | const handleFileUploaded = () => { 12 | setRefreshKey(prevKey => prevKey + 1); 13 | }; 14 | 15 | return ( 16 |
17 |

Look at Your Data

18 |
19 | 20 | 21 |
22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /nextjs/components/DataTable.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | 5 | interface RowData { 6 | id: number; 7 | [key: string]: any; 8 | } 9 | 10 | export default function DataTable() { 11 | const [data, setData] = useState([]); 12 | const [editingCell, setEditingCell] = useState<{ rowId: number; column: string } | null>(null); 13 | 14 | useEffect(() => { 15 | fetchData(); 16 | }, []); 17 | 18 | const fetchData = async () => { 19 | try { 20 | const response = await fetch('/api/data'); 21 | const jsonData = await response.json(); 22 | setData(Array.isArray(jsonData) ? jsonData : []); 23 | } catch (error) { 24 | console.error('Error fetching data:', error); 25 | setData([]); 26 | } 27 | }; 28 | 29 | const handleCellEdit = async (rowId: number, column: string, value: string) => { 30 | try { 31 | const response = await fetch('/api/data', { 32 | method: 'PUT', 33 | headers: { 'Content-Type': 'application/json' }, 34 | body: JSON.stringify({ id: rowId, column, value }), 35 | }); 36 | if (response.ok) { 37 | // Update local state immediately 38 | setData(prevData => prevData.map(row => 39 | row.id === rowId ? { ...row, [column]: value } : row 40 | )); 41 | } else { 42 | const errorData = await response.json(); 43 | alert(`Failed to update data: ${errorData.error}`); 44 | } 45 | } catch (error) { 46 | console.error('Error updating data:', error); 47 | alert('Failed to update data'); 48 | } 49 | setEditingCell(null); 50 | }; 51 | 52 | const handleDeleteRow = async (rowId: number) => { 53 | try { 54 | const response = await fetch(`/api/data?id=${rowId}`, { method: 'DELETE' }); 55 | if (response.ok) { 56 | fetchData(); 57 | } else { 58 | alert('Failed to delete row'); 59 | } 60 | } catch (error) { 61 | console.error('Error deleting row:', error); 62 | alert('Failed to delete row'); 63 | } 64 | }; 65 | 66 | if (!Array.isArray(data) || data.length === 0) return

No data available

; 67 | 68 | // Include 'id' in the columns 69 | const columns = Object.keys(data[0]); 70 | 71 | return ( 72 | 73 | 74 | 75 | {columns.map((col) => ( 76 | 79 | ))} 80 | 81 | 82 | 83 | 84 | {data.map((row) => ( 85 | 86 | {columns.map((col) => ( 87 |
77 | {col} 78 | Actions
col !== 'id' && setEditingCell({ rowId: row.id, column: col })} 91 | > 92 | {editingCell?.rowId === row.id && editingCell?.column === col ? ( 93 |