├── .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 |
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 |
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 |
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 |
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 |
69 | Name
70 | |
71 |
72 | Email
73 | |
74 |
75 | Total Invoices
76 | |
77 |
78 | Total Pending
79 | |
80 |
81 | Total Paid
82 | |
83 |
84 |
85 |
86 |
87 | {customers.map((customer) => (
88 |
89 |
90 |
91 |
98 | {customer.name}
99 |
100 | |
101 |
102 | {customer.email}
103 | |
104 |
105 | {customer.total_invoices}
106 | |
107 |
108 | {customer.total_pending}
109 | |
110 |
111 | {customer.total_paid}
112 | |
113 |
114 | ))}
115 |
116 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
61 | Customer
62 | |
63 |
64 | Email
65 | |
66 |
67 | Amount
68 | |
69 |
70 | Date
71 | |
72 |
73 | Status
74 | |
75 |
76 | Edit
77 | |
78 |
79 |
80 |
81 | {invoices?.map((invoice) => (
82 |
86 |
87 |
88 |
95 | {invoice.name}
96 |
97 | |
98 |
99 | {invoice.email}
100 | |
101 |
102 | {formatCurrency(invoice.amount)}
103 | |
104 |
105 | {formatDateToLocal(invoice.date)}
106 | |
107 |
108 |
109 | |
110 |
111 |
112 |
113 |
114 |
115 | |
116 |
117 | ))}
118 |
119 |
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 |
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 |
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 |
44 | );
45 | }
46 |
47 | export function InvoiceSkeleton() {
48 | return (
49 |
59 | );
60 | }
61 |
62 | export function LatestInvoicesSkeleton() {
63 | return (
64 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
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 |
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 |
137 | |
138 |
139 | );
140 | }
141 |
142 | export function InvoicesMobileSkeleton() {
143 | return (
144 |
163 | );
164 | }
165 |
166 | export function InvoicesTableSkeleton() {
167 | return (
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 | Customer
184 | |
185 |
186 | Email
187 | |
188 |
189 | Amount
190 | |
191 |
192 | Date
193 | |
194 |
195 | Status
196 | |
197 |
201 | Edit
202 | |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
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 | {header} |
111 | {/each}
112 | Actions |
113 |
114 |
115 |
116 | {#each tableData as row, rowIndex}
117 |
118 | {#each row as cell, colIndex}
119 |
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 | |
133 | {/each}
134 |
135 |
136 | |
137 |
138 | {/each}
139 |
140 |
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 |
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 |
77 | {col}
78 | |
79 | ))}
80 | Actions |
81 |
82 |
83 |
84 | {data.map((row) => (
85 |
86 | {columns.map((col) => (
87 | col !== 'id' && setEditingCell({ rowId: row.id, column: col })}
91 | >
92 | {editingCell?.rowId === row.id && editingCell?.column === col ? (
93 | |
108 | ))}
109 |
110 |
116 | |
117 |
118 | ))}
119 |
120 |
121 | );
122 | }
--------------------------------------------------------------------------------
/nextjs/components/DownloadButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | export default function DownloadButton() {
4 | const handleDownload = async () => {
5 | try {
6 | const response = await fetch('/api/download');
7 | const blob = await response.blob();
8 | const url = window.URL.createObjectURL(blob);
9 | const a = document.createElement('a');
10 | a.href = url;
11 | a.download = 'data.csv';
12 | document.body.appendChild(a);
13 | a.click();
14 | window.URL.revokeObjectURL(url);
15 | } catch (error) {
16 | console.error('Error downloading file:', error);
17 | alert('Failed to download file');
18 | }
19 | };
20 |
21 | return (
22 |
28 | );
29 | }
--------------------------------------------------------------------------------
/nextjs/components/FileUpload.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 |
5 | interface FileUploadProps {
6 | onFileUploaded: () => void;
7 | }
8 |
9 | export default function FileUpload({ onFileUploaded }: FileUploadProps) {
10 | const [file, setFile] = useState(null);
11 |
12 | const handleFileUpload = async (e: React.FormEvent) => {
13 | e.preventDefault();
14 | if (!file) return;
15 |
16 | const formData = new FormData();
17 | formData.append('file', file);
18 |
19 | try {
20 | const response = await fetch('/api/upload', {
21 | method: 'POST',
22 | body: formData,
23 | });
24 | if (response.ok) {
25 | alert('File uploaded successfully');
26 | onFileUploaded();
27 | } else {
28 | alert('File upload failed');
29 | }
30 | } catch (error) {
31 | console.error('Error uploading file:', error);
32 | alert('File upload failed');
33 | }
34 | };
35 |
36 | return (
37 |
48 | );
49 | }
--------------------------------------------------------------------------------
/nextjs/lib/db.ts:
--------------------------------------------------------------------------------
1 | import Database from 'better-sqlite3';
2 |
3 | const db = new Database('your_database.sqlite');
4 |
5 | export { db };
6 |
--------------------------------------------------------------------------------
/nextjs/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/nextjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@types/multer": "^1.4.12",
13 | "better-sqlite3": "^11.2.1",
14 | "csv-parse": "^5.5.6",
15 | "csv-stringify": "^6.5.1",
16 | "multer": "1.4.5-lts.1",
17 | "next": "14.2.7",
18 | "react": "^18.3.1",
19 | "react-dom": "^18.3.1",
20 | "react-table": "^7.8.0",
21 | "sqlite": "^5.1.1",
22 | "sqlite3": "^5.1.7"
23 | },
24 | "devDependencies": {
25 | "@types/better-sqlite3": "^7.6.11",
26 | "@types/node": "^20.16.5",
27 | "@types/react": "^18.3.5",
28 | "@types/react-dom": "^18.3.0",
29 | "eslint": "^8.57.0",
30 | "eslint-config-next": "14.2.7",
31 | "postcss": "^8.4.45",
32 | "tailwindcss": "^3.4.10",
33 | "typescript": "^5.5.4"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/nextjs/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/nextjs/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 | backgroundImage: {
12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
13 | "gradient-conic":
14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
15 | },
16 | },
17 | },
18 | plugins: [],
19 | };
20 | export default config;
21 |
--------------------------------------------------------------------------------
/nextjs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "framework-comparison"
3 | version = "0.1.0"
4 | description = "Add your description here"
5 | readme = "README.md"
6 | requires-python = ">=3.12"
7 | dependencies = [
8 | "fastapi>=0.112.2",
9 | "fastlite==0.0.11",
10 | "jinja2>=3.1.4",
11 | "pandas>=2.2.2",
12 | "python-fasthtml>=0.5.1",
13 | "python-multipart>=0.0.9",
14 | "uvicorn>=0.30.6",
15 | ]
16 |
17 | [tool.ruff.lint]
18 | ignore = ["F403", "F405"]
19 |
--------------------------------------------------------------------------------
/svelte/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/svelte/.prettierignore:
--------------------------------------------------------------------------------
1 | # Package Managers
2 | package-lock.json
3 | pnpm-lock.yaml
4 | yarn.lock
5 |
--------------------------------------------------------------------------------
/svelte/.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 |
--------------------------------------------------------------------------------
/svelte/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 |
--------------------------------------------------------------------------------
/svelte/data.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eugeneyan/framework-comparison/fabdca0e5b73573796988494e1b519534b210768/svelte/data.sqlite
--------------------------------------------------------------------------------
/svelte/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 |
--------------------------------------------------------------------------------
/svelte/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte",
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 | "@types/better-sqlite3": "^7.6.0"
31 | },
32 | "dependencies": {
33 | "better-sqlite3": "^8.0.0",
34 | "csv-parse": "^5.3.0",
35 | "csv-stringify": "^6.2.0"
36 | },
37 | "type": "module"
38 | }
39 |
--------------------------------------------------------------------------------
/svelte/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 |
--------------------------------------------------------------------------------
/svelte/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/svelte/src/hooks.server.ts:
--------------------------------------------------------------------------------
1 | export async function handle({ event, resolve }) {
2 | return resolve(event);
3 | }
--------------------------------------------------------------------------------
/svelte/src/lib/api.ts:
--------------------------------------------------------------------------------
1 | export async function fetchData() {
2 | const response = await fetch('/api/data');
3 | return response.json();
4 | }
5 |
6 | export async function updateRow(id: number, data: any) {
7 | const response = await fetch(`/api/data/${id}`, {
8 | method: 'PUT',
9 | headers: { 'Content-Type': 'application/json' },
10 | body: JSON.stringify(data)
11 | });
12 | return response.json();
13 | }
14 |
15 | export async function deleteRow(id: number) {
16 | const response = await fetch(`/api/data/${id}`, { method: 'DELETE' });
17 | return response.json();
18 | }
--------------------------------------------------------------------------------
/svelte/src/lib/components/CsvUpload.svelte:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/svelte/src/lib/components/Table.svelte:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
44 |
45 | {#each Object.keys(tableData[0] || {}) as header}
46 | {header} |
47 | {/each}
48 | Actions |
49 |
50 |
51 |
52 | {#each tableData as row, rowIndex}
53 |
54 | {#each Object.entries(row) as [key, value]}
55 |
56 | startEditing(rowIndex, key, value)}
60 | on:input={(e) => editedValue = e.target.textContent}
61 | on:keydown={(e) => handleKeyDown(e, row.id)}
62 | on:blur={() => saveEdit(row.id)}
63 | >
64 | {value}
65 |
66 | |
67 | {/each}
68 |
69 |
70 | |
71 |
72 | {/each}
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/svelte/src/lib/db.ts:
--------------------------------------------------------------------------------
1 | import Database from 'better-sqlite3';
2 |
3 | const db = new Database('data.sqlite');
4 |
5 | export function query(sql: string, params: any[] = []) {
6 | return db.prepare(sql).all(params);
7 | }
8 |
9 | export function run(sql: string, params: any[] = []) {
10 | return db.prepare(sql).run(params);
11 | }
12 |
13 | export { db };
--------------------------------------------------------------------------------
/svelte/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 | Look at Your Data
20 |
24 |
27 |
28 |
29 |
70 |
--------------------------------------------------------------------------------
/svelte/src/routes/api/data/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 | import { query } from '$lib/db';
3 |
4 | export async function GET() {
5 | try {
6 | const data = query('SELECT * FROM csv_data');
7 | return json(data);
8 | } catch (error) {
9 | // If the table doesn't exist yet, return an empty array
10 | if (error.message.includes('no such table')) {
11 | return json([]);
12 | }
13 | throw error;
14 | }
15 | }
--------------------------------------------------------------------------------
/svelte/src/routes/api/data/[id]/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 | import { run } from '$lib/db';
3 |
4 | export async function PUT({ params, request }) {
5 | const { id } = params;
6 | const updatedRow = await request.json();
7 |
8 | const setClause = Object.keys(updatedRow).map(col => `${col} = ?`).join(', ');
9 | const sql = `UPDATE csv_data SET ${setClause} WHERE id = ?`;
10 |
11 | run(sql, [...Object.values(updatedRow), id]);
12 | return json({ success: true });
13 | }
14 |
15 | export async function DELETE({ params }) {
16 | run('DELETE FROM csv_data WHERE id = ?', [params.id]);
17 | return json({ success: true });
18 | }
--------------------------------------------------------------------------------
/svelte/src/routes/api/download/+server.ts:
--------------------------------------------------------------------------------
1 | import { query } from '$lib/db';
2 | import { stringify } from 'csv-stringify/sync';
3 |
4 | export async function GET() {
5 | try {
6 | const csv = stringify(query('SELECT * FROM csv_data'), { header: true });
7 | return new Response(csv, {
8 | headers: {
9 | 'Content-Type': 'text/csv',
10 | 'Content-Disposition': 'attachment; filename="table_data.csv"'
11 | }
12 | });
13 | } catch (error) {
14 | if (error instanceof Error && error.message.includes('no such table')) {
15 | return new Response('', {
16 | headers: {
17 | 'Content-Type': 'text/csv',
18 | 'Content-Disposition': 'attachment; filename="table_data.csv"'
19 | }
20 | });
21 | }
22 | throw error;
23 | }
24 | }
--------------------------------------------------------------------------------
/svelte/src/routes/api/upload/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 | import { run, db } from '$lib/db';
3 | import { parse } from 'csv-parse/sync';
4 |
5 | export async function POST({ request }) {
6 | const file = await request.formData().then(data => data.get('csv') as File);
7 |
8 | if (!file) return json({ error: 'No file uploaded' }, { status: 400 });
9 |
10 | const records = parse(await file.text(), { columns: true });
11 | if (records.length === 0) return json({ error: 'Empty CSV file' }, { status: 400 });
12 |
13 | const columns = Object.keys(records[0]);
14 | run(`CREATE TABLE IF NOT EXISTS csv_data (${columns.map(col => `${col} TEXT`).join(', ')})`);
15 | run('DELETE FROM csv_data');
16 |
17 | const insertSQL = `INSERT INTO csv_data (${columns.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`;
18 | const stmt = db.prepare(insertSQL);
19 |
20 | db.transaction((records: any[]) => {
21 | for (const record of records) {
22 | stmt.run(columns.map(col => record[col]));
23 | }
24 | })(records);
25 |
26 | return json({ success: true });
27 | }
--------------------------------------------------------------------------------
/svelte/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 |
--------------------------------------------------------------------------------
/svelte/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 |
--------------------------------------------------------------------------------
/svelte/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import { defineConfig } from 'vite';
3 |
4 | export default defineConfig({
5 | plugins: [sveltekit()]
6 | });
7 |
--------------------------------------------------------------------------------