├── vite.config.ts.timestamp-1719693692326-f548269587e3a.mjs
├── .gitignore
├── env.d.ts
├── public
└── favicon.ico
├── postcss.config.js
├── app
├── styles
│ └── tailwind.css
├── routes
│ ├── db.tsx
│ ├── db.$name.tsx
│ ├── db.$name._index.tsx
│ ├── db.$name.docs.tsx
│ ├── db._index.tsx
│ ├── db.$name.history.tsx
│ ├── _index.tsx
│ ├── db.$name.create.tsx
│ └── db.$name.doc.$id.tsx
├── entry.client.tsx
├── root.tsx
└── components
│ ├── FireproofMenu.tsx
│ └── Sidebar.tsx
├── tailwind.config.js
├── vite.config.ts
├── tsconfig.json
├── package.json
├── README.md
└── .eslintrc.cjs
/vite.config.ts.timestamp-1719693692326-f548269587e3a.mjs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /build
5 | /public/build
6 | .env
7 |
--------------------------------------------------------------------------------
/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jchris/dashboard-remix/main/public/favicon.ico
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
--------------------------------------------------------------------------------
/app/styles/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer utilities {
6 | a:hover {
7 | @apply underline;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./app/**/*.{js,ts,jsx,tsx}",
5 | ],
6 | theme: {
7 | extend: {},
8 | },
9 | plugins: [],
10 | };
--------------------------------------------------------------------------------
/app/routes/db.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "react-router-dom";
2 | import { FireproofMenu } from "~/components/FireproofMenu";
3 |
4 | export default function DbIndex() {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | }
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { vitePlugin as remix } from "@remix-run/dev";
2 | import { defineConfig } from "vite";
3 | import tsconfigPaths from "vite-tsconfig-paths";
4 |
5 | export default defineConfig({
6 | plugins: [
7 | remix({
8 | ssr: false,
9 | future: {
10 | v3_fetcherPersist: true,
11 | v3_relativeSplatPath: true,
12 | v3_throwAbortReason: true,
13 | },
14 | }),
15 | tsconfigPaths(),
16 | ],
17 | });
18 |
--------------------------------------------------------------------------------
/app/routes/db.$name.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "react-router-dom";
2 | import { Sidebar } from "~/components/Sidebar";
3 |
4 | export default function Database() {
5 | return (
6 |
7 |
{/* Fixed width of 16rem (64 units) */}
8 |
9 |
10 |
{/* Flex-grow to take remaining space */}
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/app/entry.client.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * By default, Remix will handle hydrating your app on the client for you.
3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4 | * For more information, see https://remix.run/file-conventions/entry.client
5 | */
6 |
7 | import { RemixBrowser } from "@remix-run/react";
8 | import { startTransition, StrictMode } from "react";
9 | import { hydrateRoot } from "react-dom/client";
10 |
11 | startTransition(() => {
12 | hydrateRoot(
13 | document,
14 |
15 |
16 |
17 | );
18 | });
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["env.d.ts", "**/*.ts", "**/*.tsx"],
3 | "compilerOptions": {
4 | "lib": ["DOM", "DOM.Iterable", "ES2022"],
5 | "isolatedModules": true,
6 | "esModuleInterop": true,
7 | "jsx": "react-jsx",
8 | "module": "ESNext",
9 | "moduleResolution": "Bundler",
10 | "resolveJsonModule": true,
11 | "target": "ES2022",
12 | "strict": true,
13 | "allowJs": true,
14 | "skipLibCheck": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "baseUrl": ".",
17 | "paths": {
18 | "~/*": ["./app/*"]
19 | },
20 |
21 | // Remix takes care of building everything in `remix build`.
22 | "noEmit": true
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/routes/db.$name._index.tsx:
--------------------------------------------------------------------------------
1 | import { useParams } from "@remix-run/react";
2 | import { useFireproof } from "use-fireproof";
3 |
4 |
5 | export default function DbInfo() {
6 | const { name } = useParams();
7 | const { useLiveQuery, database } = useFireproof(name);
8 | const allDocs = useLiveQuery('_id')
9 | const head = database._crdt.clock.head.map(cid => cid.toString())
10 |
11 | return (
12 |
13 |
14 | Info: {name}
15 |
16 |
There are {allDocs.docs.length} documents
17 |
Head:
18 |
19 | {JSON.stringify(head, null, 2)}
20 |
21 |
22 | );
23 | }
--------------------------------------------------------------------------------
/app/root.tsx:
--------------------------------------------------------------------------------
1 | import "./styles/tailwind.css";
2 | import {
3 | Links,
4 | Meta,
5 | Outlet,
6 | Scripts,
7 | ScrollRestoration
8 | } from "@remix-run/react";
9 |
10 | export function Layout({ children }: { children: React.ReactNode }) {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {children}
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | export default function App() {
29 | return
30 | }
31 |
32 | export function HydrateFallback() {
33 | return Loading...
;
34 | }
--------------------------------------------------------------------------------
/app/routes/db.$name.docs.tsx:
--------------------------------------------------------------------------------
1 | import { useParams } from "@remix-run/react";
2 | import { Link } from "react-router-dom";
3 | import { useFireproof } from "use-fireproof";
4 |
5 |
6 | export default function AddDocuments() {
7 | const { name } = useParams();
8 | const { useLiveQuery } = useFireproof(name);
9 | const allDocs = useLiveQuery('_id')
10 |
11 | return (
12 |
13 |
All Documents
14 |
15 | These are all the documents in the {name} database.
16 |
17 |
18 | {allDocs.docs.map(({ _id }) => (
19 | -
20 |
21 | {_id}
22 |
23 |
24 | ))}
25 |
26 |
27 | );
28 | }
--------------------------------------------------------------------------------
/app/components/FireproofMenu.tsx:
--------------------------------------------------------------------------------
1 | export function FireproofMenu() {
2 | return (
3 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/routes/db._index.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | const databases = ["db1", "db2", "db3"]; // Replace with your actual database list
4 |
5 | import { Sidebar } from "~/components/Sidebar";
6 |
7 | export default function DatabaseIndex() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
20 | function DatabaseList() {
21 | return (
22 |
23 |
Available Databases
24 |
This is the index page for the /db route.
25 |
26 | {databases.map((db) => (
27 | -
28 |
29 | {db}
30 |
31 |
32 | ))}
33 |
34 |
35 | );
36 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dashboard",
3 | "private": true,
4 | "sideEffects": false,
5 | "type": "module",
6 | "scripts": {
7 | "build": "remix vite:build",
8 | "dev": "remix vite:dev",
9 | "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
10 | "preview": "vite preview",
11 | "typecheck": "tsc"
12 | },
13 | "dependencies": {
14 | "@remix-run/node": "^2.10.0",
15 | "@remix-run/react": "^2.10.0",
16 | "react": "^18.3.1",
17 | "react-dom": "^18.3.1",
18 | "use-fireproof": "^0.18.0"
19 | },
20 | "devDependencies": {
21 | "@remix-run/dev": "^2.10.0",
22 | "@types/react": "^18.3.3",
23 | "@types/react-dom": "^18.3.0",
24 | "@typescript-eslint/eslint-plugin": "^7.14.1",
25 | "@typescript-eslint/parser": "^7.14.1",
26 | "autoprefixer": "^10.4.19",
27 | "eslint": "^8.5.0",
28 | "eslint-import-resolver-typescript": "^3.6.1",
29 | "eslint-plugin-import": "^2.29.1",
30 | "eslint-plugin-jsx-a11y": "^6.9.0",
31 | "eslint-plugin-react": "^7.34.3",
32 | "eslint-plugin-react-hooks": "^4.6.2",
33 | "postcss": "^8.4.39",
34 | "tailwindcss": "^3.4.4",
35 | "typescript": "^5.5.2",
36 | "vite": "^5.3.2",
37 | "vite-tsconfig-paths": "^4.3.2"
38 | },
39 | "engines": {
40 | "node": ">=20.0.0"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/routes/db.$name.history.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useParams } from "@remix-run/react";
3 | import { Link } from "react-router-dom";
4 | import { DocBase, useFireproof } from "use-fireproof";
5 |
6 |
7 | export default function AddDocuments() {
8 | const { name } = useParams();
9 | const { database } = useFireproof(name);
10 |
11 | const [history, setHistory] = useState({ rows: [] } as { rows: { key: string; value: DocBase }[] });
12 |
13 | useEffect(() => {
14 | const handleChanges = async () => {
15 | const changes = await database.changes()
16 | setHistory(changes);
17 | };
18 |
19 | void handleChanges()
20 | return database.subscribe(handleChanges);
21 | }, [database]);
22 |
23 | return (
24 |
25 |
26 | Recent Changes in {name} Database
27 |
28 |
These are the recent changes in the database.
29 |
30 | {history.rows.map(({ key }) => (
31 | -
32 |
33 | {key}
34 |
35 |
36 | ))}
37 |
38 |
39 | );
40 | }
--------------------------------------------------------------------------------
/app/routes/_index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | export default function Index() {
5 | const [dbNames, setDbNames] = useState([]);
6 | const [newDbName, setNewDbName] = useState("");
7 |
8 | const handleAddDbName = (e: React.FormEvent) => {
9 | e.preventDefault();
10 | if (newDbName && !dbNames.includes(newDbName)) {
11 | setDbNames([...dbNames, newDbName]);
12 | setNewDbName("");
13 | }
14 | };
15 |
16 | return (
17 |
18 |
Database List
19 |
31 |
32 | {dbNames.map((name) => (
33 | -
34 |
35 | {name}
36 |
37 |
38 | ))}
39 |
40 |
41 | );
42 | }
--------------------------------------------------------------------------------
/app/routes/db.$name.create.tsx:
--------------------------------------------------------------------------------
1 | import { useParams } from "@remix-run/react";
2 | import { useState } from "react";
3 | import { useFireproof } from "use-fireproof";
4 |
5 | export default function CreateDocument() {
6 | const { name } = useParams();
7 | const { database } = useFireproof(name);
8 | const [jsonInput, setJsonInput] = useState("");
9 | const [error, setError] = useState("");
10 |
11 | const handleSave = () => {
12 | try {
13 | const parsedJson = JSON.parse(jsonInput);
14 | database.put(parsedJson);
15 | setError("");
16 | setJsonInput("");
17 | } catch (e) {
18 | setError("Invalid JSON");
19 | }
20 | };
21 |
22 | return (
23 |
24 |
Create Document
25 |
26 | Enter the JSON for the new document in the {name} database.
27 |
28 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # templates/spa
2 |
3 | This template leverages [Remix SPA Mode](https://remix.run/docs/en/main/guides/spa-mode) to build your app as a Single-Page Application using [Client Data](https://remix.run/docs/en/main/guides/client-data) for all of your data loads and mutations.
4 |
5 | ## Setup
6 |
7 | ```shellscript
8 | npx create-remix@latest --template remix-run/remix/templates/spa
9 | ```
10 |
11 | ## Development
12 |
13 | You can develop your SPA app just like you would a normal Remix app, via:
14 |
15 | ```shellscript
16 | npm run dev
17 | ```
18 |
19 | ## Production
20 |
21 | When you are ready to build a production version of your app, `npm run build` will generate your assets and an `index.html` for the SPA.
22 |
23 | ```shellscript
24 | npm run build
25 | ```
26 |
27 | ### Preview
28 |
29 | You can preview the build locally with [vite preview](https://vitejs.dev/guide/cli#vite-preview) to serve all routes via the single `index.html` file:
30 |
31 | ```shellscript
32 | npm run preview
33 | ```
34 |
35 | > [!IMPORTANT]
36 | >
37 | > `vite preview` is not designed for use as a production server
38 |
39 | ### Deployment
40 |
41 | You can then serve your app from any HTTP server of your choosing. The server should be configured to serve multiple paths from a single root `/index.html` file (commonly called "SPA fallback"). Other steps may be required if the server doesn't directly support this functionality.
42 |
43 | For a simple example, you could use [sirv-cli](https://www.npmjs.com/package/sirv-cli):
44 |
45 | ```shellscript
46 | npx sirv-cli build/client/ --single
47 | ```
48 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /**
2 | * This is intended to be a basic starting point for linting in your app.
3 | * It relies on recommended configs out of the box for simplicity, but you can
4 | * and should modify this configuration to best suit your team's needs.
5 | */
6 |
7 | /** @type {import('eslint').Linter.Config} */
8 | module.exports = {
9 | root: true,
10 | parserOptions: {
11 | ecmaVersion: "latest",
12 | sourceType: "module",
13 | ecmaFeatures: {
14 | jsx: true,
15 | },
16 | },
17 | env: {
18 | browser: true,
19 | commonjs: true,
20 | es6: true,
21 | },
22 | ignorePatterns: ["!**/.server", "!**/.client"],
23 |
24 | // Base config
25 | extends: ["eslint:recommended"],
26 |
27 | overrides: [
28 | // React
29 | {
30 | files: ["**/*.{js,jsx,ts,tsx}"],
31 | plugins: ["react", "jsx-a11y"],
32 | extends: [
33 | "plugin:react/recommended",
34 | "plugin:react/jsx-runtime",
35 | "plugin:react-hooks/recommended",
36 | "plugin:jsx-a11y/recommended",
37 | ],
38 | settings: {
39 | react: {
40 | version: "detect",
41 | },
42 | formComponents: ["Form"],
43 | linkComponents: [
44 | { name: "Link", linkAttribute: "to" },
45 | { name: "NavLink", linkAttribute: "to" },
46 | ],
47 | "import/resolver": {
48 | typescript: {},
49 | },
50 | },
51 | },
52 |
53 | // Typescript
54 | {
55 | files: ["**/*.{ts,tsx}"],
56 | plugins: ["@typescript-eslint", "import"],
57 | parser: "@typescript-eslint/parser",
58 | settings: {
59 | "import/internal-regex": "^~/",
60 | "import/resolver": {
61 | node: {
62 | extensions: [".ts", ".tsx"],
63 | },
64 | typescript: {
65 | alwaysTryTypes: true,
66 | },
67 | },
68 | },
69 | extends: [
70 | "plugin:@typescript-eslint/recommended",
71 | "plugin:import/recommended",
72 | "plugin:import/typescript",
73 | ],
74 | },
75 |
76 | // Node
77 | {
78 | files: [".eslintrc.cjs"],
79 | env: {
80 | node: true,
81 | },
82 | },
83 | ],
84 | };
85 |
--------------------------------------------------------------------------------
/app/routes/db.$name.doc.$id.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "@remix-run/react";
2 | import { useParams } from "react-router-dom";
3 | import { DocBase, useFireproof } from "use-fireproof";
4 | import { useState } from "react";
5 |
6 | export default function Document() {
7 | const { name, id } = useParams();
8 | const { useDocument, database } = useFireproof(name);
9 | const [isEditing, setIsEditing] = useState(false);
10 |
11 | const [doc] = useDocument(() => ({ _id: id! }));
12 |
13 | const handleSave = (updatedDoc: DocBase) => {
14 | setIsEditing(false);
15 | database.put(updatedDoc);
16 | };
17 |
18 | return (
19 |
20 |
21 | Document: {id}
22 |
23 |
24 | Database: {name}
25 |
26 | {isEditing ? (
27 |
28 | ) : (
29 |
30 |
31 | {JSON.stringify(doc, null, 2)}
32 |
33 |
36 |
37 | )}
38 |
39 | );
40 | }
41 |
42 | export function EditableArea({ doc, onSave }: { doc: DocBase, onSave: (updatedDoc: DocBase) => void }) {
43 | const [jsonInput, setJsonInput] = useState(JSON.stringify(doc, null, 2));
44 | const [error, setError] = useState("");
45 |
46 | const handleSaveClick = () => {
47 | try {
48 | const parsedJson = JSON.parse(jsonInput);
49 | onSave(parsedJson);
50 | setError("");
51 | } catch (e) {
52 | setError("Invalid JSON");
53 | }
54 | };
55 |
56 | return (
57 |
69 | );
70 | }
--------------------------------------------------------------------------------
/app/components/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { useParams, Link } from 'react-router-dom'
2 |
3 | export function Sidebar() {
4 | const { name: dbName } = useParams()
5 | return (
6 |
7 |
8 | -
9 |
13 |
28 |
29 | All Databases
30 |
31 |
32 |
33 | -
34 | {dbName && (
35 |
36 | -
37 |
41 |
56 |
57 |
{dbName}
58 |
59 |
60 |
61 | -
62 |
66 |
80 |
81 |
82 | Create Document
83 |
84 |
85 |
86 | -
87 |
91 |
105 |
106 | History
107 |
108 |
109 |
110 | -
111 |
115 |
129 |
130 | Query
131 |
132 |
133 |
134 |
135 | )}
136 |
137 |
138 |
139 | )
140 | }
141 |
--------------------------------------------------------------------------------