├── .node-version ├── css ├── frontend.scss ├── fonts.scss ├── icon.scss ├── progress.scss ├── notification.scss ├── path.scss ├── card.scss ├── modal.scss ├── aside.scss ├── table.scss ├── admin.scss ├── button.scss ├── navbar.scss └── forms.scss ├── public ├── _headers ├── favicon.ico └── _routes.json ├── remix.env.d.ts ├── .gitignore ├── .eslintrc.js ├── types ├── cloudflare.d.ts ├── build.d.ts └── remix.d.ts ├── postcss.config.js ├── remix.config.js ├── README.md ├── tailwind.config.js ├── app ├── entry.client.tsx ├── components │ └── tools.tsx ├── tsconfig.json ├── utils.ts ├── entry.server.tsx ├── routes │ ├── dash.tsx │ ├── _index.tsx │ ├── customer.$id.tsx │ ├── products.tsx │ ├── product.$id.tsx │ ├── supplier.$id.tsx │ ├── suppliers.tsx │ ├── customers.tsx │ ├── employees.tsx │ ├── orders.tsx │ ├── employee.$id.tsx │ ├── search.tsx │ └── order.$id.tsx └── root.tsx ├── functions ├── tsconfig.json └── [[remix]].ts ├── wrangler.toml ├── package.json └── db └── schema.sql /.node-version: -------------------------------------------------------------------------------- 1 | 16.13.0 2 | -------------------------------------------------------------------------------- /css/frontend.scss: -------------------------------------------------------------------------------- 1 | /* color definitions */ 2 | $nav-bg-color: #eeeeee; 3 | 4 | -------------------------------------------------------------------------------- /public/_headers: -------------------------------------------------------------------------------- 1 | /build/* 2 | Cache-Control: public, max-age=31536000, s-maxage=31536000 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-d1-northwind/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /.wrangler 5 | /build 6 | /public/build 7 | .env 8 | /app/css/admin.css -------------------------------------------------------------------------------- /public/_routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "include": ["/*"], 4 | "exclude": ["/build/*", "/favicon.ico"] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], 4 | }; 5 | -------------------------------------------------------------------------------- /types/cloudflare.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface Env { 4 | CF_PAGES?: 1; 5 | DB: D1Database; 6 | SESSION_SECRET: string; 7 | } 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "postcss-nested": {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /css/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Inter'; 3 | font-style: normal; 4 | font-weight: 400; 5 | font-display: swap; 6 | src: url(https://fonts.gstatic.com/s/inter/v1/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfMZg.ttf) format('truetype'); 7 | } 8 | -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@remix-run/dev').AppConfig} */ 2 | module.exports = { 3 | ignoredRouteFiles: ["**/.*"], 4 | serverDependenciesToBundle: [/~/], 5 | serverModuleFormat: "esm", 6 | future: { 7 | v2_routeConvention: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /css/icon.scss: -------------------------------------------------------------------------------- 1 | .material-icons { 2 | @apply inline-flex justify-center items-center w-6 h-6; 3 | } 4 | 5 | .material-icons.widget-icon { 6 | @apply w-20 h-20; 7 | } 8 | 9 | .material-icons.large { 10 | @apply w-12 h-12; 11 | } 12 | 13 | .material-icons span { 14 | @apply inline-flex; 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Remix! 2 | 3 | - [Remix Docs](https://remix.run/docs) 4 | 5 | ## Development 6 | 7 | From your terminal: 8 | 9 | ```sh 10 | npm run dev 11 | ``` 12 | 13 | This starts your app in development mode, rebuilding assets on file changes. 14 | 15 | ## Deployment 16 | 17 | Wire it up via the CF Dashboard. More info to come. 18 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require("tailwindcss/colors"); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: ["app/**/*.{ts,tsx}"], 6 | theme: { 7 | extend: { 8 | colors: { 9 | sky: colors.sky, 10 | cyan: colors.cyan, 11 | }, 12 | }, 13 | }, 14 | plugins: [], 15 | }; 16 | -------------------------------------------------------------------------------- /css/progress.scss: -------------------------------------------------------------------------------- 1 | progress { 2 | @apply h-3 rounded-full overflow-hidden; 3 | } 4 | 5 | progress::-webkit-progress-bar { 6 | @apply bg-gray-200; 7 | } 8 | 9 | progress::-webkit-progress-value { 10 | @apply bg-green-500; 11 | } 12 | 13 | progress::-moz-progress-bar { 14 | @apply bg-green-500; 15 | } 16 | 17 | progress::-ms-fill { 18 | @apply bg-green-500 border-0; 19 | } -------------------------------------------------------------------------------- /css/notification.scss: -------------------------------------------------------------------------------- 1 | .notification { 2 | @apply px-3 py-6 rounded; 3 | } 4 | 5 | .notification:not(:last-child) { 6 | @apply mb-6; 7 | } 8 | 9 | .notification.blue { 10 | @apply bg-blue-500 text-white; 11 | } 12 | 13 | .notification.green { 14 | @apply bg-green-500 text-white; 15 | } 16 | 17 | .notification.red { 18 | @apply bg-red-500 text-white; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /css/path.scss: -------------------------------------------------------------------------------- 1 | .is-title-bar { 2 | @apply p-6 border-b border-gray-100; 3 | } 4 | 5 | .is-title-bar li { 6 | @apply inline-block pr-3 text-2xl text-gray-500; 7 | } 8 | 9 | .is-title-bar li:not(:last-child):after { 10 | content: "/"; 11 | @apply inline-block pl-3; 12 | } 13 | 14 | .is-title-bar li:last-child { 15 | @apply pr-0 font-black text-black; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /types/build.d.ts: -------------------------------------------------------------------------------- 1 | import { type ServerBuild } from "@remix-run/cloudflare"; 2 | 3 | export const assets: ServerBuild["assets"]; 4 | export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"]; 5 | export const entry: ServerBuild["entry"]; 6 | export const future: ServerBuild["future"]; 7 | export const publicPath: ServerBuild["publicPath"]; 8 | export const routes: ServerBuild["routes"]; 9 | -------------------------------------------------------------------------------- /types/remix.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import "@remix-run/server-runtime"; 5 | import { type Session } from "@remix-run/server-runtime"; 6 | 7 | declare module "@remix-run/server-runtime" { 8 | export interface AppLoadContext { 9 | cf?: IncomingRequestCfProperties; 10 | DB: D1Database; 11 | session: Session; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { RemixBrowser } from "@remix-run/react"; 2 | import { startTransition, StrictMode } from "react"; 3 | import { hydrateRoot } from "react-dom/client"; 4 | 5 | function hydrate() { 6 | startTransition(() => { 7 | hydrateRoot( 8 | document, 9 | 10 | 11 | 12 | ); 13 | }); 14 | } 15 | 16 | if (typeof requestIdleCallback === "function") { 17 | requestIdleCallback(hydrate); 18 | } else { 19 | // Safari doesn't support requestIdleCallback 20 | // https://caniuse.com/requestidlecallback 21 | setTimeout(hydrate, 1); 22 | } 23 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["../types/cloudflare.d.ts", "**/*.ts"], 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "lib": ["esnext"], 7 | "isolatedModules": true, 8 | "esModuleInterop": true, 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "~/*": ["../app/*"], 16 | "#build": ["../types/build.d.ts", "../build/index.js"] 17 | }, 18 | 19 | // Wrangler takes care of building everything in the `functions` director. 20 | "noEmit": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "remix-d1-northwind" 2 | compatibility_date = "2023-01-18" 3 | compatibility_flags = ["streams_enable_constructors"] 4 | 5 | [[env.local.d1_databases]] 6 | binding = "DB" 7 | database_name = "d1-northwind" 8 | database_id = "1" 9 | 10 | [[env.dev.d1_databases]] 11 | binding = "DB" 12 | database_name = "d1-northwind-staging" 13 | database_id = "b98e08e0-bb6b-4b15-b7af-33f3b215f577" 14 | 15 | [[env.production.d1_databases]] 16 | binding = "DB" 17 | database_name = "d1-northwind" 18 | database_id = "699ec5da-e0a9-44d1-91d3-7b1daae10736" 19 | 20 | [[d1_databases]] 21 | binding = "DB" 22 | database_name = "d1-northwind" 23 | database_id = "699ec5da-e0a9-44d1-91d3-7b1daae10736" 24 | -------------------------------------------------------------------------------- /app/components/tools.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@remix-run/react"; 2 | 3 | export function AddTableField(props: { 4 | name: string; 5 | link?: string; 6 | value: string | number; 7 | }) { 8 | return ( 9 |
10 | 11 |
12 |
13 |
14 | {props.link ? ( 15 | 16 | {props.value} 17 | 18 | ) : ( 19 | `${props.value}` 20 | )} 21 |
22 |
23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["../types/remix.d.ts", "**/*.ts", "**/*.tsx"], 3 | "exclude": ["functions/**/*"], 4 | "compilerOptions": { 5 | "target": "ES2019", 6 | "module": "ESNext", 7 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 8 | "types": ["@cloudflare/workers-types"], 9 | "isolatedModules": true, 10 | "esModuleInterop": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "jsx": "react-jsx", 16 | "baseUrl": ".", 17 | "paths": { 18 | "~/*": ["./*"] 19 | }, 20 | 21 | // Remix takes care of building everything in the `app` directory. 22 | "noEmit": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/utils.ts: -------------------------------------------------------------------------------- 1 | import { defer, type Session } from "@remix-run/cloudflare"; 2 | 3 | export async function maybeDefer>( 4 | session: Session, 5 | data: T 6 | ) { 7 | const delay = Number(session.get("delay") || 0); 8 | const shouldDefer = Boolean(session.get("defer")); 9 | 10 | if (!shouldDefer) { 11 | await new Promise((resolve) => setTimeout(resolve, delay)); 12 | } 13 | 14 | for (const [key, value] of Object.entries(data)) { 15 | if (value instanceof Promise) { 16 | if (shouldDefer) { 17 | (data as any)[key] = new Promise((resolve) => 18 | setTimeout(resolve, delay) 19 | ).then(() => value); 20 | } else { 21 | (data as any)[key] = await value; 22 | } 23 | } 24 | } 25 | 26 | return defer(data); 27 | } 28 | -------------------------------------------------------------------------------- /css/card.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | @apply bg-white border border-gray-100 rounded; 3 | } 4 | 5 | .card.has-table .card-content { 6 | @apply p-0; 7 | } 8 | 9 | .card-content { 10 | @apply p-6; 11 | } 12 | 13 | .card-content hr { 14 | @apply my-6 -mx-6; 15 | } 16 | 17 | .card.empty .card-content { 18 | @apply text-center py-12 text-gray-500; 19 | } 20 | 21 | .card-header { 22 | @apply flex items-stretch border-b border-gray-100; 23 | } 24 | 25 | .card-header-title, .card-header-icon { 26 | @apply flex items-center py-3 px-4; 27 | } 28 | 29 | .card-header-title { 30 | @apply flex-grow font-bold ; 31 | } 32 | 33 | .card-header-icon { 34 | @apply justify-center; 35 | } 36 | 37 | .widget-label h3 { 38 | @apply text-lg leading-tight text-gray-500; 39 | } 40 | 41 | .widget-label h1 { 42 | @apply text-3xl leading-tight font-semibold; 43 | } 44 | 45 | .form-screen .card { 46 | @apply w-11/12 lg:w-5/12 shadow-2xl rounded-lg; 47 | } -------------------------------------------------------------------------------- /css/modal.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | @apply hidden items-center flex-col justify-center overflow-hidden fixed inset-0 z-40; 3 | } 4 | 5 | .modal.active { 6 | @apply flex; 7 | } 8 | 9 | .modal-background { 10 | @apply absolute inset-0 bg-gray-900 bg-opacity-80; 11 | } 12 | 13 | .modal-card { 14 | max-height: calc(100vh - 160px); 15 | @apply w-full flex flex-col overflow-hidden relative 16 | lg:mx-auto lg:w-2/5; 17 | } 18 | 19 | .modal-card-body { 20 | @apply bg-white flex-grow flex-shrink overflow-auto p-6 space-y-2; 21 | } 22 | 23 | .modal-card-head, 24 | .modal-card-foot { 25 | @apply flex items-center flex-shrink-0 justify-start px-6 py-4 relative bg-gray-100 border-gray-200; 26 | } 27 | 28 | .modal-card-head { 29 | @apply border-b rounded-t; 30 | } 31 | 32 | .modal-card-foot { 33 | @apply border-t rounded-b; 34 | } 35 | 36 | .modal-card-foot .button:not(:last-child) { 37 | @apply mr-2; 38 | } 39 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { type EntryContext } from "@remix-run/cloudflare"; 2 | import { RemixServer } from "@remix-run/react"; 3 | import isbot from "isbot"; 4 | import { renderToReadableStream } from "react-dom/server"; 5 | 6 | export default async function handleRequest( 7 | request: Request, 8 | responseStatusCode: number, 9 | responseHeaders: Headers, 10 | remixContext: EntryContext 11 | ) { 12 | const body = await renderToReadableStream( 13 | , 14 | { 15 | onError(error) { 16 | console.error("renderToReadableStream error"); 17 | console.error(error); 18 | responseStatusCode = 500; 19 | }, 20 | } 21 | ); 22 | 23 | if (isbot(request.headers.get("user-agent"))) { 24 | await body.allReady; 25 | } 26 | 27 | responseHeaders.set("Content-Type", "text/html"); 28 | return new Response(body, { 29 | headers: responseHeaders, 30 | status: responseStatusCode, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /functions/[[remix]].ts: -------------------------------------------------------------------------------- 1 | import { 2 | createRequestHandler, 3 | createCookieSessionStorage, 4 | } from "@remix-run/cloudflare"; 5 | import { type AppLoadContext } from "@remix-run/server-runtime"; 6 | 7 | import * as build from "#build"; 8 | 9 | let remixHandler: ReturnType; 10 | 11 | export const onRequest: PagesFunction = async (ctx) => { 12 | if (!remixHandler) { 13 | remixHandler = createRequestHandler( 14 | build as any, 15 | ctx.env.CF_PAGES ? "production" : "development" 16 | ); 17 | } 18 | 19 | const sessionStorage = createCookieSessionStorage({ 20 | cookie: { 21 | httpOnly: true, 22 | path: "/", 23 | secure: Boolean(ctx.request.url.match(/^(http|ws)s:\/\//)), 24 | secrets: [ctx.env.SESSION_SECRET], 25 | }, 26 | }); 27 | 28 | const session = await sessionStorage.getSession( 29 | ctx.request.headers.get("Cookie") 30 | ); 31 | 32 | const loadContext: AppLoadContext = { 33 | cf: ctx.request.cf, 34 | DB: ctx.env.DB, 35 | session, 36 | }; 37 | 38 | const response = await remixHandler(ctx.request, loadContext); 39 | response.headers.set( 40 | "Set-Cookie", 41 | await sessionStorage.commitSession(session) 42 | ); 43 | 44 | return response; 45 | }; 46 | -------------------------------------------------------------------------------- /css/aside.scss: -------------------------------------------------------------------------------- 1 | .aside { 2 | @apply w-60 -left-60 fixed top-0 z-40 h-screen bg-gray-800 transition-all lg:left-0; 3 | } 4 | 5 | .aside-tools { 6 | @apply flex flex-row w-full bg-gray-900 text-white flex-1 px-3 h-14 items-center; 7 | } 8 | 9 | .aside-mobile-expanded .aside { 10 | @apply left-0; 11 | } 12 | 13 | .aside-mobile-expanded .navbar { 14 | @apply ml-60; 15 | } 16 | 17 | .aside-mobile-expanded #app { 18 | @apply ml-60; 19 | } 20 | 21 | .aside-mobile-expanded, 22 | .aside-mobile-expanded body { 23 | @apply overflow-hidden lg:overflow-visible; 24 | } 25 | 26 | .menu-label { 27 | @apply p-3 text-xs uppercase text-gray-400; 28 | } 29 | 30 | .menu-clock { 31 | @apply p-3 text-xs uppercase text-white; 32 | } 33 | 34 | .menu-list li a { 35 | @apply py-2 flex text-gray-300; 36 | } 37 | 38 | .menu-list li > a { 39 | @apply hover:bg-gray-700; 40 | } 41 | 42 | .menu-list li a .menu-item-label { 43 | @apply flex-grow; 44 | } 45 | 46 | .menu-list li a .icon { 47 | @apply w-12 flex-none; 48 | } 49 | 50 | .menu-list li > a.active { 51 | @apply bg-gray-700; 52 | } 53 | 54 | .menu-list li.active > a { 55 | @apply bg-gray-700; 56 | } 57 | 58 | .menu-list li ul a { 59 | @apply p-3 text-sm; 60 | } 61 | 62 | .menu-list li.active ul { 63 | @apply block bg-gray-600; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /css/table.scss: -------------------------------------------------------------------------------- 1 | table { 2 | @apply w-full; 3 | } 4 | 5 | thead { 6 | @apply hidden lg:table-header-group; 7 | } 8 | 9 | tr { 10 | @apply max-w-full block relative border-b-4 border-gray-100 11 | lg:table-row lg:border-b-0; 12 | } 13 | 14 | tr:last-child { 15 | @apply border-b-0; 16 | } 17 | 18 | th { 19 | @apply lg:text-left lg:py-2 lg:px-3; 20 | } 21 | 22 | td { 23 | @apply flex justify-between text-right py-3 px-4 align-top border-b border-gray-100 24 | lg:table-cell lg:text-left lg:py-2 lg:px-3 lg:align-middle lg:border-b-0; 25 | } 26 | 27 | tr:nth-child(odd) td { 28 | @apply lg:bg-gray-50; 29 | } 30 | 31 | td:last-child { 32 | @apply border-b-0; 33 | } 34 | 35 | tbody tr:hover td { 36 | @apply lg:bg-gray-100; 37 | } 38 | 39 | td:before { 40 | content: attr(data-label); 41 | @apply font-semibold pr-3 text-left lg:hidden; 42 | } 43 | 44 | td.checkbox-cell, 45 | th.checkbox-cell { 46 | @apply lg:w-5; 47 | } 48 | 49 | td.progress-cell progress { 50 | @apply flex w-2/5 self-center 51 | lg:w-full; 52 | } 53 | 54 | td.image-cell { 55 | @apply border-b-0 lg:w-6; 56 | } 57 | 58 | td.image-cell:before, 59 | td.actions-cell:before { 60 | @apply hidden; 61 | } 62 | 63 | td.image-cell .image { 64 | @apply w-24 h-24 mx-auto lg:w-6 lg:h-6; 65 | } 66 | 67 | .table-pagination { 68 | @apply px-6 py-3 border-t border-gray-100; 69 | } 70 | 71 | -------------------------------------------------------------------------------- /app/routes/dash.tsx: -------------------------------------------------------------------------------- 1 | import { json, type LoaderArgs } from "@remix-run/cloudflare"; 2 | import { useLoaderData } from "@remix-run/react"; 3 | 4 | export function loader({ context: { cf } }: LoaderArgs) { 5 | return json({ 6 | colo: cf?.colo || "unknown", 7 | country: (cf && "country" in cf ? cf?.country : null) || "country", 8 | }); 9 | } 10 | 11 | export default function Dashboard() { 12 | const { colo, country } = useLoaderData(); 13 | 14 | return ( 15 |
16 |
17 |
18 |

Worker

19 |

Colo: {colo}

20 |

Country: {country}

21 |
22 | {/*
23 |

SQL Metrics

24 |

Query count: {stats.queries}

25 |

26 | Results count: {stats.results} 27 |

28 |

# SELECT: {stats.select}

29 |

30 | # SELECT WHERE: {stats.select_where} 31 |

32 |

33 | # SELECT LEFT JOIN: {stats.select_leftjoin} 34 |

35 |
*/} 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "sideEffects": false, 4 | "scripts": { 5 | "build": "npm run build:css && npm run build:remix", 6 | "build:css": "postcss ./css/admin.scss -o ./app/css/admin.css", 7 | "build:remix": "remix build", 8 | "dev": "npm run build:css && concurrently \"npm:dev:*\"", 9 | "dev:css": "postcss ./css/admin.scss -o ./app/css/admin.css --watch", 10 | "dev:remix": "remix watch", 11 | "dev:cloudflare": "wrangler pages dev public --compatibility-date=2023-01-18 --persist --d1 DB --binding SESSION_SECRET=s --env local --live-reload", 12 | "typecheck": "tsc" 13 | }, 14 | "dependencies": { 15 | "@remix-run/cloudflare": "^1.11.0", 16 | "@remix-run/react": "^1.11.0", 17 | "@remix-run/serve": "^1.11.0", 18 | "isbot": "^3.6.5", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0" 21 | }, 22 | "devDependencies": { 23 | "@cloudflare/workers-types": "*", 24 | "@remix-run/dev": "^1.11.0", 25 | "@remix-run/eslint-config": "^1.11.0", 26 | "@types/react": "^18.0.25", 27 | "@types/react-dom": "^18.0.8", 28 | "autoprefixer": "^10.4.13", 29 | "better-sqlite3": "^8.0.1", 30 | "concurrently": "^7.6.0", 31 | "eslint": "^8.27.0", 32 | "postcss": "^8.4.21", 33 | "postcss-cli": "^10.1.0", 34 | "postcss-import": "^15.1.0", 35 | "postcss-nested": "^6.0.0", 36 | "tailwindcss": "^3.2.4", 37 | "typescript": "^4.8.4", 38 | "wrangler": "^2.8.0" 39 | }, 40 | "engines": { 41 | "node": ">=14" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /css/admin.scss: -------------------------------------------------------------------------------- 1 | @import "aside.scss"; 2 | @import "button.scss"; 3 | @import "card.scss"; 4 | @import "forms.scss"; 5 | @import "icon.scss"; 6 | @import "modal.scss"; 7 | @import "navbar.scss"; 8 | @import "notification.scss"; 9 | @import "path.scss"; 10 | @import "progress.scss"; 11 | @import "table.scss"; 12 | 13 | @tailwind base; 14 | @tailwind components; 15 | @tailwind utilities; 16 | 17 | body { 18 | @apply bg-gray-50 text-base pt-14 lg:pl-60; 19 | } 20 | 21 | #app { 22 | @apply w-screen transition-all lg:w-auto; 23 | } 24 | 25 | .video { 26 | position: relative; 27 | padding-top: 56.25%; 28 | } 29 | 30 | .console { 31 | height: 600px; 32 | } 33 | 34 | .link { 35 | @apply text-blue-600; 36 | } 37 | 38 | .main-section { 39 | @apply p-6; 40 | 41 | animation: fadeIn ease 0.5s; 42 | opacity: 1; 43 | } 44 | 45 | .dropdown { 46 | @apply cursor-pointer; 47 | } 48 | 49 | .clipped, 50 | .clipped body { 51 | @apply overflow-hidden; 52 | } 53 | 54 | .m-clipped, 55 | .m-clipped body { 56 | @apply overflow-hidden lg:overflow-visible; 57 | } 58 | 59 | .form-screen body { 60 | @apply p-0; 61 | } 62 | 63 | .form-screen .main-section { 64 | @apply flex h-screen items-center justify-center; 65 | } 66 | 67 | .fade-out { 68 | animation: fadeOut ease 0.5s; 69 | opacity: 0; 70 | } 71 | @keyframes fadeOut { 72 | 0% { 73 | opacity: 1; 74 | } 75 | 100% { 76 | opacity: 0; 77 | } 78 | } 79 | 80 | @keyframes fadeIn { 81 | 0% { 82 | opacity: 0; 83 | } 84 | 100% { 85 | opacity: 1; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /css/button.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | @apply inline-flex bg-white text-black border-gray-50 border cursor-pointer justify-center px-4 py-2 text-center 3 | whitespace-nowrap rounded 4 | hover:border-gray-500 5 | focus:outline-none; 6 | } 7 | 8 | .button.addon-right { 9 | @apply rounded-r-none; 10 | } 11 | 12 | .button.addon-left { 13 | @apply rounded-l-none; 14 | } 15 | 16 | .button.small { 17 | @apply text-xs p-1; 18 | } 19 | 20 | .button.small.textual { 21 | @apply px-3; 22 | } 23 | 24 | .button.active { 25 | @apply border-gray-300 hover:border-gray-500; 26 | } 27 | 28 | .button.green { 29 | @apply bg-green-500 border-green-500 text-white hover:bg-green-600; 30 | } 31 | 32 | .button.indigo { 33 | @apply bg-indigo-500 border-indigo-500 text-white hover:bg-indigo-600; 34 | } 35 | 36 | .button.red { 37 | @apply bg-red-500 border-red-500 text-white hover:bg-red-600; 38 | } 39 | 40 | .button.pink { 41 | @apply bg-pink-500 border-pink-500 text-white hover:bg-pink-600; 42 | } 43 | 44 | .button.yellow { 45 | @apply bg-yellow-500 border-yellow-500 text-black hover:bg-yellow-600; 46 | } 47 | 48 | .button.blue { 49 | @apply bg-blue-500 border-blue-500 text-white hover:bg-blue-600; 50 | } 51 | 52 | .button.light { 53 | @apply bg-gray-100 border-gray-100 hover:bg-gray-200; 54 | } 55 | 56 | .buttons { 57 | @apply flex items-center flex-wrap justify-start; 58 | } 59 | 60 | .buttons.nowrap { 61 | @apply flex-nowrap; 62 | } 63 | 64 | .buttons.right { 65 | @apply justify-end; 66 | } 67 | 68 | .buttons .button { 69 | @apply mx-1; 70 | } 71 | -------------------------------------------------------------------------------- /app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | export default function Index() { 2 | return ( 3 | <> 4 |
5 |

Welcome to Northwind Traders

6 |
7 |

Running on Cloudflare's D1

8 |
9 | 14 |

15 | This is a demo of the Northwind dataset, running on{" "} 16 | 21 | Cloudflare Workers 22 | 23 | , and D1 - Cloudflare's newest SQL database, running on SQLite. 24 |

25 |

26 | Read our{" "} 27 | 32 | D1 announcement 33 | {" "} 34 | to learn more about D1. 35 |

36 |

37 | This dataset was sourced from{" "} 38 | 43 | northwind-SQLite3 44 | 45 | . 46 |

47 |

48 | You can use the UI to explore Supplies, Orders, Customers, Employees 49 | and Products, or you can use search if you know what you're looking 50 | for. 51 |

52 | 53 | {/* 54 |