Merge pull request #2 from selimaj-dev/done

Done
This commit is contained in:
Klesti Selimaj
2026-01-29 21:25:04 +01:00
committed by GitHub
15 changed files with 4880 additions and 139 deletions
+4
View File
@@ -0,0 +1,4 @@
.react-router
build
node_modules
README.md
+5 -137
View File
@@ -1,139 +1,7 @@
# Logs .DS_Store
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env .env
.env.* /node_modules/
!.env.example
# parcel-bundler cache (https://parceljs.org/) # React Router
.cache /.react-router/
.parcel-cache /build/
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Sveltekit cache directory
.svelte-kit/
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Firebase cache directory
.firebase/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+22
View File
@@ -0,0 +1,22 @@
FROM node:20-alpine AS development-dependencies-env
COPY . /app
WORKDIR /app
RUN npm ci
FROM node:20-alpine AS production-dependencies-env
COPY ./package.json package-lock.json /app/
WORKDIR /app
RUN npm ci --omit=dev
FROM node:20-alpine AS build-env
COPY . /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN npm run build
FROM node:20-alpine
COPY ./package.json package-lock.json /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
WORKDIR /app
CMD ["npm", "run", "start"]
+87 -2
View File
@@ -1,2 +1,87 @@
# 3dplot-gen # Welcome to React Router!
A 3D plot generator online tool
A modern, production-ready template for building full-stack React applications using React Router.
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
## Features
- 🚀 Server-side rendering
- ⚡️ Hot Module Replacement (HMR)
- 📦 Asset bundling and optimization
- 🔄 Data loading and mutations
- 🔒 TypeScript by default
- 🎉 TailwindCSS for styling
- 📖 [React Router docs](https://reactrouter.com/)
## Getting Started
### Installation
Install the dependencies:
```bash
npm install
```
### Development
Start the development server with HMR:
```bash
npm run dev
```
Your application will be available at `http://localhost:5173`.
## Building for Production
Create a production build:
```bash
npm run build
```
## Deployment
### Docker Deployment
To build and run using Docker:
```bash
docker build -t my-app .
# Run the container
docker run -p 3000:3000 my-app
```
The containerized application can be deployed to any platform that supports Docker, including:
- AWS ECS
- Google Cloud Run
- Azure Container Apps
- Digital Ocean App Platform
- Fly.io
- Railway
### DIY Deployment
If you're familiar with deploying Node applications, the built-in app server is production-ready.
Make sure to deploy the output of `npm run build`
```
├── package.json
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
├── build/
│ ├── client/ # Static assets
│ └── server/ # Server-side code
```
## Styling
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
---
Built with ❤️ using React Router.
+15
View File
@@ -0,0 +1,15 @@
@import "tailwindcss";
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
html,
body {
@apply bg-neutral-900 text-slate-200;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
}
+75
View File
@@ -0,0 +1,75 @@
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";
import type { Route } from "./+types/root";
import "./app.css";
export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "Oops!";
let details = "An unexpected error occurred.";
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? "404" : "Error";
details =
error.status === 404
? "The requested page could not be found."
: error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}
return (
<main className="pt-16 p-4 container mx-auto">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre className="w-full p-4 overflow-x-auto">
<code>{stack}</code>
</pre>
)}
</main>
);
}
+3
View File
@@ -0,0 +1,3 @@
import { type RouteConfig, index } from "@react-router/dev/routes";
export default [index("routes/home.tsx")] satisfies RouteConfig;
+432
View File
@@ -0,0 +1,432 @@
import type React from "react";
import { useEffect, useRef, useState } from "react";
import * as papa from "papaparse";
import type { Route } from "./+types/home";
export function meta({ }: Route.MetaArgs) {
return [
{ title: "3dplot-gen" },
{ name: "description", content: "CSV → 3D Plot generator" },
];
}
type Row = Record<string, number>;
type PlotType = "scatter" | "surface";
type ThemeScheme = "light" | "dark" | "transparentBlack" | "transparentWhite";
const THEME_SCHEMES: Record<
ThemeScheme,
{
paperBg: string;
plotBg: string;
fontColor: string;
gridColor: string;
mainClass: string;
}
> = {
dark: {
paperBg: "#0b0d12",
plotBg: "#0b0d12",
fontColor: "#cfd3ff",
gridColor: "rgba(148, 163, 184, 0.25)",
mainClass: "bg-neutral-950",
},
light: {
paperBg: "#f8fafc",
plotBg: "#f8fafc",
fontColor: "#1e293b",
gridColor: "rgba(0, 0, 0, 0.25)",
mainClass: "bg-slate-100",
},
transparentBlack: {
paperBg: "rgba(0,0,0,0)",
plotBg: "rgba(0,0,0,0)",
fontColor: "#475569",
gridColor: "rgba(0, 0, 0, 0.25)",
mainClass: "bg-transparent",
},
transparentWhite: {
paperBg: "rgba(0,0,0,0)",
plotBg: "rgba(0,0,0,0)",
fontColor: "#475569",
gridColor: "rgba(100, 116, 139, 0.4)",
mainClass: "bg-transparent",
},
};
const COLORSCALES = [
"Viridis",
"Plasma",
"Inferno",
"Magma",
"Cividis",
"Hot",
"Electric",
"Bluered",
"Portland",
"Rainbow",
"RdBu",
"Earth",
"Teal",
"Turbo",
] as const;
export default function Home() {
const plotRef = useRef<HTMLDivElement>(null);
const [rows, setRows] = useState<Row[]>([]);
const [columns, setColumns] = useState<string[]>([]);
const [x, setX] = useState("");
const [y, setY] = useState("");
const [z, setZ] = useState("");
const [plotType, setPlotType] = useState<PlotType>("scatter");
const [colorscale, setColorscale] = useState<string>(COLORSCALES[0]);
const [themeScheme, setThemeScheme] = useState<ThemeScheme>("dark");
// First row after header = axis sizes (X max, Y max, Z max); rest = plot data.
const plotData = rows.length > 1 ? rows.slice(1) : rows;
const axisSizes =
rows.length > 1 && x && y && z
? (() => {
const sx = Number(rows[0][x]);
const sy = Number(rows[0][y]);
const sz = Number(rows[0][z]);
return [sx, sy, sz].every((n) => Number.isFinite(n))
? { x: sx, y: sy, z: sz }
: null;
})()
: null;
// Build surface grid from scattered (x,y,z) points. Returns null if data isn't a grid.
function buildSurfaceGrid(): {
xUnique: number[];
yUnique: number[];
zMatrix: number[][];
} | null {
const xVals = plotData.map((r) => Number(r[x]));
const yVals = plotData.map((r) => Number(r[y]));
const zVals = plotData.map((r) => Number(r[z]));
const xUnique = [...new Set(xVals)].sort((a, b) => a - b);
const yUnique = [...new Set(yVals)].sort((a, b) => a - b);
const keyToZ = new Map<string, number>();
for (let i = 0; i < plotData.length; i++) {
const k = `${xVals[i]},${yVals[i]}`;
if (!keyToZ.has(k)) keyToZ.set(k, zVals[i]);
}
if (xUnique.length * yUnique.length !== keyToZ.size) return null;
const zMatrix: number[][] = yUnique.map((yi) =>
xUnique.map((xj) => keyToZ.get(`${xj},${yi}`) ?? NaN)
);
return { xUnique, yUnique, zMatrix };
}
// Render plot when axes or options change
useEffect(() => {
if (!plotRef.current || !x || !y || !z) return;
let cancelled = false;
(async () => {
const Plotly = await import("plotly.js-dist-min");
if (cancelled) return;
const xVals = plotData.map((r) => Number(r[x]));
const yVals = plotData.map((r) => Number(r[y]));
const zVals = plotData.map((r) => Number(r[z]));
const grid = buildSurfaceGrid();
const useSurface = plotType === "surface" && grid !== null;
const trace = useSurface && grid
? {
type: "surface" as const,
x: grid.xUnique,
y: grid.yUnique,
z: grid.zMatrix,
colorscale: colorscale as "Viridis",
colorbar: { title: { text: z } },
}
: {
type: "scatter3d" as const,
mode: "markers" as const,
x: xVals,
y: yVals,
z: zVals,
marker: {
size: 4,
color: zVals,
colorscale: colorscale as "Viridis",
},
};
const theme = THEME_SCHEMES[themeScheme];
const sceneLayout: {
bgcolor?: string;
xaxis: {
title: { text: string };
range?: [number, number];
gridcolor?: string;
tickfont?: { color: string };
};
yaxis: {
title: { text: string };
range?: [number, number];
gridcolor?: string;
tickfont?: { color: string };
};
zaxis: {
title: { text: string };
range?: [number, number];
gridcolor?: string;
tickfont?: { color: string };
};
} = {
bgcolor: theme.plotBg,
xaxis: {
title: { text: x },
gridcolor: theme.gridColor,
tickfont: { color: theme.fontColor },
},
yaxis: {
title: { text: y },
gridcolor: theme.gridColor,
tickfont: { color: theme.fontColor },
},
zaxis: {
title: { text: z },
gridcolor: theme.gridColor,
tickfont: { color: theme.fontColor },
},
};
if (axisSizes) {
sceneLayout.xaxis.range = [0, axisSizes.x];
sceneLayout.yaxis.range = [0, axisSizes.y];
sceneLayout.zaxis.range = [0, axisSizes.z];
}
Plotly.newPlot(
plotRef.current!,
[trace],
{
paper_bgcolor: theme.paperBg,
plot_bgcolor: theme.plotBg,
font: { color: theme.fontColor },
scene: sceneLayout,
margin: { l: 0, r: 0, t: 0, b: 0 },
},
{ responsive: true }
);
})();
return () => {
cancelled = true;
};
}, [rows, x, y, z, plotType, colorscale, themeScheme]);
function onFile(file: File) {
papa.parse(file, {
header: true,
dynamicTyping: true,
skipEmptyLines: true,
complete: (res) => {
const data = (res.data as Row[]).filter(
(row) => row && Object.keys(row).length > 0
);
if (!data.length) {
setRows([]);
setColumns([]);
setX("");
setY("");
setZ("");
return;
}
const cols = Object.keys(data[0]);
setRows(data);
setColumns(cols);
if (cols.length >= 3) {
setX(cols[0]);
setY(cols[1]);
setZ(cols[2]);
} else {
setX("");
setY("");
setZ("");
}
},
});
}
async function exportPNG() {
const Plotly = await import("plotly.js-dist-min");
if (!plotRef.current) return;
Plotly.toImage(plotRef.current, {
format: "png",
width: 1600,
height: 900,
}).then((url) => {
const a = document.createElement("a");
a.href = url;
a.download = "3dplot-gen.png";
a.click();
});
}
return (
<div className="h-screen grid grid-cols-[280px_1fr] overflow-y-hidden">
{/* Sidebar */}
<aside className="bg-panel p-4 space-y-4 overflow-y-scroll">
<div className="flex items-center justify-between">
<div className="text-lg font-semibold">
3D plot generator <a href="https://github.com/selimaj-dev" className="text-sm text-neutral-400 block w-max">by Klesti Selimaj</a>
<div className="text-xs text-neutral-500">CSV to 3D Plot</div>
</div>
<a href="https://github.com/selimaj-dev/3dplot-gen" className="text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="lucide lucide-github-icon lucide-github"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" /><path d="M9 18c-4.51 2-5-2-7-2" /></svg>
</a>
</div>
<Control label="CSV file">
<input
type="file"
accept=".csv"
onChange={(e) =>
e.target.files && onFile(e.target.files[0])
}
className="w-full text-sm file:bg-neutral-900 file:text-slate-300 file:border-0
file:rounded file:px-2 file:py-1"
/>
</Control>
<p className="text-xs text-muted">
First data row = axis sizes (X max, Y max, Z max). Plot uses rows after that.{" "}
<a href="/example-surface.csv" className="text-accent underline" download>
Example (surface)
</a>
</p>
<Control label="X axis">
<select
className="w-full bg-neutral-900 border border-slate-700 rounded px-2 py-1"
value={x}
onChange={(e) => setX(e.target.value)}
>
<option value="">—</option>
{columns.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</Control>
<Control label="Y axis">
<select
className="w-full bg-neutral-900 border border-slate-700 rounded px-2 py-1"
value={y}
onChange={(e) => setY(e.target.value)}
>
<option value="">—</option>
{columns.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</Control>
<Control label="Z axis">
<select
className="w-full bg-neutral-900 border border-slate-700 rounded px-2 py-1"
value={z}
onChange={(e) => setZ(e.target.value)}
>
<option value="">—</option>
{columns.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</Control>
<Control label="Plot type">
<select
className="w-full bg-neutral-900 border border-slate-700 rounded px-2 py-1"
value={plotType}
onChange={(e) => setPlotType(e.target.value as PlotType)}
>
<option value="scatter">Dots (scatter)</option>
<option value="surface">Surface (area)</option>
</select>
</Control>
<Control label="Colorscale">
<select
className="w-full bg-neutral-900 border border-slate-700 rounded px-2 py-1"
value={colorscale}
onChange={(e) => setColorscale(e.target.value)}
>
{COLORSCALES.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
</Control>
<Control label="Background">
<select
className="w-full bg-neutral-900 border border-slate-700 rounded px-2 py-1"
value={themeScheme}
onChange={(e) => setThemeScheme(e.target.value as ThemeScheme)}
>
<option value="dark">Dark</option>
<option value="light">Light</option>
<option value="transparentBlack">Transparent Black Grid</option>
<option value="transparentWhite">Transparent White Grid</option>
</select>
</Control>
{plotType === "surface" &&
plotData.length > 0 &&
buildSurfaceGrid() === null && (
<p className="text-xs text-amber-500/90">
Surface needs a full X and Y grid of points. Showing dots instead.
</p>
)}
<button
onClick={exportPNG}
className="w-full mt-2 bg-accent text-sm rounded px-2 py-1 hover:opacity-90"
>
Export PNG
</button>
</aside>
{/* Plot */}
<main className={THEME_SCHEMES[themeScheme].mainClass}>
<div ref={plotRef} className="w-full h-full" />
</main>
</div>
);
}
function Control({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) {
return (
<div className="space-y-1">
<label className="text-xs text-muted">{label}</label>
{children}
</div>
);
}
+4078
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
{
"name": "3dplot-gen",
"private": true,
"type": "module",
"scripts": {
"build": "react-router build",
"dev": "react-router dev",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc"
},
"dependencies": {
"@react-router/node": "7.12.0",
"@react-router/serve": "7.12.0",
"@types/papaparse": "^5.5.2",
"@types/plotly.js-dist-min": "^2.3.4",
"isbot": "^5.1.31",
"papaparse": "^5.5.3",
"plotly.js-dist-min": "^3.3.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router": "7.12.0"
},
"devDependencies": {
"@react-router/dev": "7.12.0",
"@tailwindcss/vite": "^4.1.13",
"@types/node": "^22.19.7",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"tailwindcss": "^4.1.13",
"typescript": "^5.9.2",
"vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4"
}
}
+83
View File
@@ -0,0 +1,83 @@
X,Y,Z
2,2,6
0,0,1.000
0.25,0,1.109
0.5,0,1.206
0.75,0,1.291
1,0,1.365
1.25,0,1.428
1.5,0,1.480
1.75,0,1.521
2,0,1.550
0,0.25,1.175
0.25,0.25,1.304
0.5,0.25,1.421
0.75,0.25,1.526
1,0.25,1.620
1.25,0.25,1.703
1.5,0.25,1.774
1.75,0.25,1.834
2,0.25,1.883
0,0.5,1.331
0.25,0.5,1.479
0.5,0.5,1.615
0.75,0.5,1.739
1,0.5,1.851
1.25,0.5,1.952
1.5,0.5,2.041
1.75,0.5,2.119
2,0.5,2.185
0,0.75,1.469
0.25,0.75,1.634
0.5,0.75,1.787
0.75,0.75,1.928
1,0.75,2.057
1.25,0.75,2.174
1.5,0.75,2.279
1.75,0.75,2.372
2,0.75,2.454
0,1,1.590
0.25,1,1.772
0.5,1,1.942
0.75,1,2.099
1,1,2.245
1.25,1,2.378
1.5,1,2.500
1.75,1,2.610
2,1,2.708
0,1.25,1.694
0.25,1.25,1.892
0.5,1.25,2.078
0.75,1.25,2.251
1,1.25,2.413
1.25,1.25,2.562
1.5,1.25,2.700
1.75,1.25,2.825
2,1.25,2.939
0,1.5,1.781
0.25,1.5,1.994
0.5,1.5,2.196
0.75,1.5,2.384
1,1.5,2.561
1.25,1.5,2.725
1.5,1.5,2.878
1.75,1.5,3.018
2,1.5,3.146
0,1.75,1.851
0.25,1.75,2.079
0.5,1.75,2.295
0.75,1.75,2.499
1,1.75,2.692
1.25,1.75,2.872
1.5,1.75,3.041
1.75,1.75,3.197
2,1.75,3.341
0,2,1.905
0.25,2,2.146
0.5,2,2.376
0.75,2,2.594
1,2,2.801
1.25,2,2.995
1.5,2,3.178
1.75,2,3.349
2,2,3.508
1 X Y Z
2 2 2 6
3 0 0 1.000
4 0.25 0 1.109
5 0.5 0 1.206
6 0.75 0 1.291
7 1 0 1.365
8 1.25 0 1.428
9 1.5 0 1.480
10 1.75 0 1.521
11 2 0 1.550
12 0 0.25 1.175
13 0.25 0.25 1.304
14 0.5 0.25 1.421
15 0.75 0.25 1.526
16 1 0.25 1.620
17 1.25 0.25 1.703
18 1.5 0.25 1.774
19 1.75 0.25 1.834
20 2 0.25 1.883
21 0 0.5 1.331
22 0.25 0.5 1.479
23 0.5 0.5 1.615
24 0.75 0.5 1.739
25 1 0.5 1.851
26 1.25 0.5 1.952
27 1.5 0.5 2.041
28 1.75 0.5 2.119
29 2 0.5 2.185
30 0 0.75 1.469
31 0.25 0.75 1.634
32 0.5 0.75 1.787
33 0.75 0.75 1.928
34 1 0.75 2.057
35 1.25 0.75 2.174
36 1.5 0.75 2.279
37 1.75 0.75 2.372
38 2 0.75 2.454
39 0 1 1.590
40 0.25 1 1.772
41 0.5 1 1.942
42 0.75 1 2.099
43 1 1 2.245
44 1.25 1 2.378
45 1.5 1 2.500
46 1.75 1 2.610
47 2 1 2.708
48 0 1.25 1.694
49 0.25 1.25 1.892
50 0.5 1.25 2.078
51 0.75 1.25 2.251
52 1 1.25 2.413
53 1.25 1.25 2.562
54 1.5 1.25 2.700
55 1.75 1.25 2.825
56 2 1.25 2.939
57 0 1.5 1.781
58 0.25 1.5 1.994
59 0.5 1.5 2.196
60 0.75 1.5 2.384
61 1 1.5 2.561
62 1.25 1.5 2.725
63 1.5 1.5 2.878
64 1.75 1.5 3.018
65 2 1.5 3.146
66 0 1.75 1.851
67 0.25 1.75 2.079
68 0.5 1.75 2.295
69 0.75 1.75 2.499
70 1 1.75 2.692
71 1.25 1.75 2.872
72 1.5 1.75 3.041
73 1.75 1.75 3.197
74 2 1.75 3.341
75 0 2 1.905
76 0.25 2 2.146
77 0.5 2 2.376
78 0.75 2 2.594
79 1 2 2.801
80 1.25 2 2.995
81 1.5 2 3.178
82 1.75 2 3.349
83 2 2 3.508
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+7
View File
@@ -0,0 +1,7 @@
import type { Config } from "@react-router/dev/config";
export default {
// Config options...
// Server-side render by default, to enable SPA mode set this to `false`
ssr: true,
} satisfies Config;
+27
View File
@@ -0,0 +1,27 @@
{
"include": [
"**/*",
"**/.server/**/*",
"**/.client/**/*",
".react-router/types/**/*"
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client"],
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"rootDirs": [".", "./.react-router/types"],
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
},
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true
}
}
+8
View File
@@ -0,0 +1,8 @@
import { reactRouter } from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
});