diff --git a/heat-stack/.env.example b/heat-stack/.env.example
index 78348c0c..d117323c 100644
--- a/heat-stack/.env.example
+++ b/heat-stack/.env.example
@@ -6,3 +6,9 @@ SESSION_SECRET="super-duper-s3cret"
INTERNAL_COMMAND_TOKEN="some-made-up-token"
RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh"
SENTRY_DSN="your-dsn"
+
+# the mocks and some code rely on these two being prefixed with "MOCK_"
+# if they aren't then the real github api will be attempted
+GITHUB_CLIENT_ID="MOCK_GITHUB_CLIENT_ID"
+GITHUB_CLIENT_SECRET="MOCK_GITHUB_CLIENT_SECRET"
+GITHUB_TOKEN="MOCK_GITHUB_TOKEN"
diff --git a/heat-stack/.eslintrc.cjs b/heat-stack/.eslintrc.cjs
index 02ea66e7..5dae40cd 100644
--- a/heat-stack/.eslintrc.cjs
+++ b/heat-stack/.eslintrc.cjs
@@ -10,6 +10,8 @@ module.exports = {
'prettier',
],
rules: {
+ // playwright requires destructuring in fixtures even if you don't use anything 🤷♂️
+ 'no-empty-pattern': 'off',
'@typescript-eslint/consistent-type-imports': [
'warn',
{
@@ -18,7 +20,22 @@ module.exports = {
fixStyle: 'inline-type-imports',
},
],
- 'import/no-duplicates': 'warn',
+ 'import/no-duplicates': ['warn', { 'prefer-inline': true }],
+ 'import/consistent-type-specifier-style': ['warn', 'prefer-inline'],
+ 'import/order': [
+ 'warn',
+ {
+ alphabetize: { order: 'asc', caseInsensitive: true },
+ groups: [
+ 'builtin',
+ 'external',
+ 'internal',
+ 'parent',
+ 'sibling',
+ 'index',
+ ],
+ },
+ ],
},
overrides: [
{
diff --git a/heat-stack/.github/workflows/deploy.yml b/heat-stack/.github/workflows/deploy.yml
index 98e1a737..8201fa35 100644
--- a/heat-stack/.github/workflows/deploy.yml
+++ b/heat-stack/.github/workflows/deploy.yml
@@ -111,7 +111,16 @@ jobs:
uses: actions/cache@v3
with:
path: prisma/data.db
- key: db-cache
+ key:
+ db-cache-schema_${{ hashFiles('./prisma/schema.prisma')
+ }}-migrations_${{ hashFiles('./prisma/migrations/*/migration.sql')
+ }}
+
+ - name: 🌱 Seed Database
+ if: steps.db-cache.outputs.cache-hit != 'true'
+ run: npx prisma db seed
+ env:
+ MINIMAL_SEED: true
- name: 🏗 Build
run: npm run build
@@ -153,20 +162,20 @@ jobs:
mv ./other/Dockerfile ./Dockerfile
mv ./other/.dockerignore ./.dockerignore
+ - name: 🎈 Setup Fly
+ uses: superfly/flyctl-actions/setup-flyctl@v1.4
+
- name: 🚀 Deploy Staging
if: ${{ github.ref == 'refs/heads/dev' }}
- uses: superfly/flyctl-actions@1.3
- with:
- args:
- 'deploy --remote-only --build-arg COMMIT_SHA=${{ github.sha }} --app
- ${{ steps.app_name.outputs.value }}-staging'
+ run:
+ flyctl deploy --remote-only --build-arg COMMIT_SHA=${{ github.sha }}
+ --app ${{ steps.app_name.outputs.value }}-staging
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- name: 🚀 Deploy Production
if: ${{ github.ref == 'refs/heads/main' }}
- uses: superfly/flyctl-actions@1.3
- with:
- args: 'deploy --remote-only --build-arg COMMIT_SHA=${{ github.sha }}'
+ run:
+ flyctl deploy --remote-only --build-arg COMMIT_SHA=${{ github.sha }}
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
diff --git a/heat-stack/.gitignore b/heat-stack/.gitignore
index 4ad5c350..f9d5ad84 100644
--- a/heat-stack/.gitignore
+++ b/heat-stack/.gitignore
@@ -1,5 +1,5 @@
node_modules
-.DS_Store
+.DS_store
/build
/public/build
diff --git a/heat-stack/.prettierrc.cjs b/heat-stack/.prettierrc.js
similarity index 83%
rename from heat-stack/.prettierrc.cjs
rename to heat-stack/.prettierrc.js
index dca7b09d..b0ffa1c7 100644
--- a/heat-stack/.prettierrc.cjs
+++ b/heat-stack/.prettierrc.js
@@ -1,4 +1,5 @@
-module.exports = {
+/** @type {import("prettier").Options} */
+export default {
arrowParens: 'avoid',
bracketSameLine: false,
bracketSpacing: true,
@@ -25,5 +26,5 @@ module.exports = {
},
},
],
- plugins: [require('prettier-plugin-tailwindcss')],
+ plugins: ['prettier-plugin-tailwindcss'],
}
diff --git a/heat-stack/.vscode/extensions.json b/heat-stack/.vscode/extensions.json
index 248df137..7619ac2b 100644
--- a/heat-stack/.vscode/extensions.json
+++ b/heat-stack/.vscode/extensions.json
@@ -1,8 +1,11 @@
{
"recommendations": [
+ "bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"prisma.prisma",
- "bradlc.vscode-tailwindcss"
+ "qwtel.sqlite-viewer",
+ "yoavbls.pretty-ts-errors",
+ "github.vscode-github-actions"
]
}
diff --git a/heat-stack/.vscode/settings.json b/heat-stack/.vscode/settings.json
index eed0b2e4..374cf10c 100644
--- a/heat-stack/.vscode/settings.json
+++ b/heat-stack/.vscode/settings.json
@@ -4,9 +4,16 @@
"@remix-run/router",
"express",
"@radix-ui/**",
+ "@react-email/**",
+ "react-router-dom",
"react-router",
"stream/consumers",
- "node:stream/consumers"
+ "node:stream/consumers",
+ "node:test",
+ "console",
+ "node:console"
],
- "tailwindCSS.experimental.classRegex": [["cn\\(([^)]*)\\)"]]
+ "workbench.editorAssociations": {
+ "*.db": "sqlite-viewer.view"
+ }
}
diff --git a/heat-stack/README.md b/heat-stack/README.md
index b45b3fef..fabe59d7 100644
--- a/heat-stack/README.md
+++ b/heat-stack/README.md
@@ -60,7 +60,7 @@ To re-create the patch for py file support in `/patch`, use these [instructions]
- `npx patch-package @remix-run/dev`
- it should auto-apply any time you do `npm install`, but it may get out of sync with upstream
-## Epic Stack docs:
+### Epic Stack Docs:
@@ -75,7 +75,7 @@ To re-create the patch for py file support in `/patch`, use these [instructions]
```sh
-npx create-remix@latest --typescript --install --template epicweb-dev/epic-stack
+npx create-remix@latest --install --template epicweb-dev/epic-stack
```
[![The Epic Stack](https://github-production-user-asset-6210df.s3.amazonaws.com/1500684/246885449-1b00286c-aa3d-44b2-9ef2-04f694eb3592.png)](https://www.epicweb.dev/epic-stack)
diff --git a/heat-stack/app/components/WeatherExample.tsx b/heat-stack/app/components/WeatherExample.tsx
new file mode 100644
index 00000000..2f2b5c48
--- /dev/null
+++ b/heat-stack/app/components/WeatherExample.tsx
@@ -0,0 +1,56 @@
+import test from "@playwright/test";
+import { json } from "@remix-run/node"; // or cloudflare/deno
+import { useLoaderData } from "@remix-run/react";
+
+export async function loader() {
+ const href = 'https://archive-api.open-meteo.com/v1/archive?latitude=52.52&longitude=13.41&daily=temperature_2m_max&timezone=America%2FNew_York&start_date=2022-01-01&end_date=2023-08-30&temperature_unit=fahrenheit';
+ const res = await fetch(href);
+ return json(await res.json());
+}
+
+export function WeatherExample() {
+
+ const gists = useLoaderData();
+ return (
+
+ );
+}
+
+// import { useFetcher } from "@remix-run/react";
+// import { useEffect } from "react";
+
+// export function WeatherExample() {
+
+// // 2 separate routes - including one that doesn't load
+// // the python to avoid the long restart times.
+
+// /*
+// Historical archival records API: https://archive-api.open-meteo.com/v1/archive?latitude=52.52&longitude=13.41&daily=temperature_2m_max&timezone=America%2FNew_York&start_date=2022-01-01&end_date=2023-08-30&temperature_unit=fahrenheit
+// Current week forecast API: https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&daily=temperature_2m_max&timezone=America%2FNew_York&past_days=5&forecast_days=1&temperature_unit=fahrenheit
+// */
+// const fetcher = useFetcher();
+// const href = 'https://archive-api.open-meteo.com/v1/archive?latitude=52.52&longitude=13.41&daily=temperature_2m_max&timezone=America%2FNew_York&start_date=2022-01-01&end_date=2023-08-30&temperature_unit=fahrenheit';
+
+// // trigger the fetch with these
+// // ;
+
+// // fetcher.load(href)
+
+// useEffect(() => {
+// // fetcher.submit(data, options);
+// fetcher.load(href);
+// }, []);
+
+// // // build UI with these
+// // fetcher.state;
+// // fetcher.formMethod;
+// // fetcher.formAction;
+// // fetcher.formData;
+// // fetcher.formEncType;
+// // fetcher.data;
+// return (
+// { fetcher.data }
+// )
+// }
diff --git a/heat-stack/app/components/confetti.tsx b/heat-stack/app/components/confetti.tsx
index fb8b9038..69fbecef 100644
--- a/heat-stack/app/components/confetti.tsx
+++ b/heat-stack/app/components/confetti.tsx
@@ -1,18 +1,15 @@
import { Index as ConfettiShower } from 'confetti-react'
import { ClientOnly } from 'remix-utils'
-/**
- * confetti is a unique random identifier which re-renders the component
- */
-export function Confetti({ confetti }: { confetti?: string }) {
- if (!confetti) return null
+export function Confetti({ id }: { id?: string | null }) {
+ if (!id) return null
return (
{() => (
| null | undefined
diff --git a/heat-stack/app/components/search-bar.tsx b/heat-stack/app/components/search-bar.tsx
index 71378782..859c52d6 100644
--- a/heat-stack/app/components/search-bar.tsx
+++ b/heat-stack/app/components/search-bar.tsx
@@ -1,9 +1,9 @@
import { Form, useSearchParams, useSubmit } from '@remix-run/react'
+import { useDebounce, useIsPending } from '#app/utils/misc.tsx'
import { Icon } from './ui/icon.tsx'
import { Input } from './ui/input.tsx'
import { Label } from './ui/label.tsx'
import { StatusButton } from './ui/status-button.tsx'
-import { useDebounce, useIsSubmitting } from '~/utils/misc.tsx'
export function SearchBar({
status,
@@ -16,7 +16,7 @@ export function SearchBar({
}) {
const [searchParams] = useSearchParams()
const submit = useSubmit()
- const isSubmitting = useIsSubmitting({
+ const isSubmitting = useIsPending({
formMethod: 'GET',
formAction: '/users',
})
diff --git a/heat-stack/app/components/toaster.tsx b/heat-stack/app/components/toaster.tsx
new file mode 100644
index 00000000..62125303
--- /dev/null
+++ b/heat-stack/app/components/toaster.tsx
@@ -0,0 +1,22 @@
+import { useEffect } from 'react'
+import { Toaster, toast as showToast } from 'sonner'
+import { type Toast } from '#app/utils/toast.server.ts'
+
+export function EpicToaster({ toast }: { toast?: Toast | null }) {
+ return (
+ <>
+
+ {toast ? : null}
+ >
+ )
+}
+
+function ShowToast({ toast }: { toast: Toast }) {
+ const { id, type, title, description } = toast
+ useEffect(() => {
+ setTimeout(() => {
+ showToast[type](title, { id, description })
+ }, 0)
+ }, [description, id, title, type])
+ return null
+}
diff --git a/heat-stack/app/components/ui/button.tsx b/heat-stack/app/components/ui/button.tsx
index bb37bae5..87f29af4 100644
--- a/heat-stack/app/components/ui/button.tsx
+++ b/heat-stack/app/components/ui/button.tsx
@@ -1,8 +1,8 @@
-import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
+import * as React from 'react'
-import { cn } from '~/utils/misc.tsx'
+import { cn } from '#app/utils/misc.tsx'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
diff --git a/heat-stack/app/components/ui/checkbox.tsx b/heat-stack/app/components/ui/checkbox.tsx
index 7c1761f5..637a7fdd 100644
--- a/heat-stack/app/components/ui/checkbox.tsx
+++ b/heat-stack/app/components/ui/checkbox.tsx
@@ -1,7 +1,7 @@
-import * as React from 'react'
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
+import * as React from 'react'
-import { cn } from '~/utils/misc.tsx'
+import { cn } from '#app/utils/misc.tsx'
export type CheckboxProps = Omit<
React.ComponentPropsWithoutRef,
diff --git a/heat-stack/app/components/ui/dropdown-menu.tsx b/heat-stack/app/components/ui/dropdown-menu.tsx
index 3cb7b842..3bb4fe3a 100644
--- a/heat-stack/app/components/ui/dropdown-menu.tsx
+++ b/heat-stack/app/components/ui/dropdown-menu.tsx
@@ -1,7 +1,7 @@
-import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
+import * as React from 'react'
-import { cn } from '~/utils/misc.tsx'
+import { cn } from '#app/utils/misc.tsx'
const DropdownMenu = DropdownMenuPrimitive.Root
diff --git a/heat-stack/app/components/ui/icon.tsx b/heat-stack/app/components/ui/icon.tsx
index 7e845902..fb0108fa 100644
--- a/heat-stack/app/components/ui/icon.tsx
+++ b/heat-stack/app/components/ui/icon.tsx
@@ -1,8 +1,8 @@
import { type SVGProps } from 'react'
-import { cn } from '~/utils/misc.tsx'
+import { cn } from '#app/utils/misc.tsx'
+import { type IconName } from '@/icon-name'
import href from './icons/sprite.svg'
-import { type IconName } from '@/icon-name'
export { href }
export { IconName }
diff --git a/heat-stack/app/components/ui/icons/name.d.ts b/heat-stack/app/components/ui/icons/name.d.ts
index cfdff207..68302432 100644
--- a/heat-stack/app/components/ui/icons/name.d.ts
+++ b/heat-stack/app/components/ui/icons/name.d.ts
@@ -1,25 +1,30 @@
// This file is generated by npm run build:icons
export type IconName =
- | "arrow-left"
- | "arrow-right"
- | "avatar"
- | "camera"
- | "check"
- | "clock"
- | "cross-1"
- | "envelope-closed"
- | "exit"
- | "file-text"
- | "laptop"
- | "lock-closed"
- | "lock-open-1"
- | "magnifying-glass"
- | "moon"
- | "pencil-1"
- | "pencil-2"
- | "plus"
- | "reset"
- | "sun"
- | "trash"
- | "update";
+ | 'arrow-left'
+ | 'arrow-right'
+ | 'avatar'
+ | 'camera'
+ | 'check'
+ | 'clock'
+ | 'cross-1'
+ | 'dots-horizontal'
+ | 'download'
+ | 'envelope-closed'
+ | 'exit'
+ | 'file-text'
+ | 'github-logo'
+ | 'laptop'
+ | 'link-2'
+ | 'lock-closed'
+ | 'lock-open-1'
+ | 'magnifying-glass'
+ | 'moon'
+ | 'pencil-1'
+ | 'pencil-2'
+ | 'plus'
+ | 'question-mark-circled'
+ | 'reset'
+ | 'sun'
+ | 'trash'
+ | 'update'
diff --git a/heat-stack/app/components/ui/icons/sprite.svg b/heat-stack/app/components/ui/icons/sprite.svg
index 957a4c19..81f92f7b 100644
--- a/heat-stack/app/components/ui/icons/sprite.svg
+++ b/heat-stack/app/components/ui/icons/sprite.svg
@@ -51,6 +51,20 @@
fill="currentColor"
>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{}
diff --git a/heat-stack/app/components/ui/label.tsx b/heat-stack/app/components/ui/label.tsx
index d741010c..ec453ee2 100644
--- a/heat-stack/app/components/ui/label.tsx
+++ b/heat-stack/app/components/ui/label.tsx
@@ -1,8 +1,8 @@
-import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { cva, type VariantProps } from 'class-variance-authority'
+import * as React from 'react'
-import { cn } from '~/utils/misc.tsx'
+import { cn } from '#app/utils/misc.tsx'
const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
diff --git a/heat-stack/app/components/ui/status-button.tsx b/heat-stack/app/components/ui/status-button.tsx
index 0e6cf2ae..1bfdb61b 100644
--- a/heat-stack/app/components/ui/status-button.tsx
+++ b/heat-stack/app/components/ui/status-button.tsx
@@ -1,7 +1,7 @@
import * as React from 'react'
-import { Button, type ButtonProps } from './button.tsx'
-import { cn } from '~/utils/misc.tsx'
import { useSpinDelay } from 'spin-delay'
+import { cn } from '#app/utils/misc.tsx'
+import { Button, type ButtonProps } from './button.tsx'
import { Icon } from './icon.tsx'
import {
Tooltip,
@@ -48,7 +48,7 @@ export const StatusButton = React.forwardRef<
className={cn('flex justify-center gap-4', className)}
{...props}
>
- {children}
+ {children}
{message ? (
diff --git a/heat-stack/app/components/ui/textarea.tsx b/heat-stack/app/components/ui/textarea.tsx
index 5d35e5e2..f7719e85 100644
--- a/heat-stack/app/components/ui/textarea.tsx
+++ b/heat-stack/app/components/ui/textarea.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
-import { cn } from '~/utils/misc.tsx'
+import { cn } from '#app/utils/misc.tsx'
export interface TextareaProps
extends React.TextareaHTMLAttributes {}
diff --git a/heat-stack/app/components/ui/toast.tsx b/heat-stack/app/components/ui/toast.tsx
deleted file mode 100644
index 6ab49139..00000000
--- a/heat-stack/app/components/ui/toast.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-import * as React from 'react'
-import * as ToastPrimitives from '@radix-ui/react-toast'
-import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '~/utils/misc.tsx'
-import { Icon } from './icon.tsx'
-
-const ToastProvider = ToastPrimitives.Provider
-
-const ToastViewport = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-ToastViewport.displayName = ToastPrimitives.Viewport.displayName
-
-const toastVariants = cva(
- 'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
- {
- variants: {
- variant: {
- default: 'border bg-background',
- destructive:
- 'destructive group border-destructive bg-destructive text-destructive-foreground',
- },
- },
- defaultVariants: {
- variant: 'default',
- },
- },
-)
-
-const Toast = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef &
- VariantProps
->(({ className, variant, ...props }, ref) => {
- return (
-
- )
-})
-Toast.displayName = ToastPrimitives.Root.displayName
-
-const ToastAction = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-ToastAction.displayName = ToastPrimitives.Action.displayName
-
-const ToastClose = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-))
-ToastClose.displayName = ToastPrimitives.Close.displayName
-
-const ToastTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-ToastTitle.displayName = ToastPrimitives.Title.displayName
-
-const ToastDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-ToastDescription.displayName = ToastPrimitives.Description.displayName
-
-type ToastProps = React.ComponentPropsWithoutRef
-
-type ToastActionElement = React.ReactElement
-
-export {
- type ToastProps,
- type ToastActionElement,
- ToastProvider,
- ToastViewport,
- Toast,
- ToastTitle,
- ToastDescription,
- ToastClose,
- ToastAction,
-}
diff --git a/heat-stack/app/components/ui/toaster.tsx b/heat-stack/app/components/ui/toaster.tsx
deleted file mode 100644
index 0273b9ac..00000000
--- a/heat-stack/app/components/ui/toaster.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import {
- Toast,
- ToastClose,
- ToastDescription,
- ToastProvider,
- ToastTitle,
- ToastViewport,
-} from '~/components/ui/toast.tsx'
-import { useToast } from '~/components/ui/use-toast.ts'
-
-export function Toaster() {
- const { toasts } = useToast()
-
- return (
-
- {toasts.map(function ({ id, title, description, action, ...props }) {
- return (
-
-
- {title && {title}}
- {description && (
- {description}
- )}
-
- {action}
-
-
- )
- })}
-
-
- )
-}
diff --git a/heat-stack/app/components/ui/tooltip.tsx b/heat-stack/app/components/ui/tooltip.tsx
index d0daf73a..5017f3ef 100644
--- a/heat-stack/app/components/ui/tooltip.tsx
+++ b/heat-stack/app/components/ui/tooltip.tsx
@@ -1,7 +1,7 @@
-import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
+import * as React from 'react'
-import { cn } from '~/utils/misc.tsx'
+import { cn } from '#app/utils/misc.tsx'
const TooltipProvider = TooltipPrimitive.Provider
diff --git a/heat-stack/app/components/ui/use-toast.ts b/heat-stack/app/components/ui/use-toast.ts
deleted file mode 100644
index be3a3f76..00000000
--- a/heat-stack/app/components/ui/use-toast.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-// Inspired by react-hot-toast library
-import * as React from 'react'
-
-import type { ToastActionElement, ToastProps } from '~/components/ui/toast.tsx'
-
-const TOAST_LIMIT = 4
-const TOAST_REMOVE_DELAY = 1000000
-
-type ToasterToast = ToastProps & {
- id: string
- title?: React.ReactNode
- description?: React.ReactNode
- action?: ToastActionElement
-}
-
-const actionTypes = {
- ADD_TOAST: 'ADD_TOAST',
- UPDATE_TOAST: 'UPDATE_TOAST',
- DISMISS_TOAST: 'DISMISS_TOAST',
- REMOVE_TOAST: 'REMOVE_TOAST',
-} as const
-
-let count = 0
-
-function genId() {
- count = (count + 1) % Number.MAX_VALUE
- return count.toString()
-}
-
-type ActionType = typeof actionTypes
-
-type Action =
- | {
- type: ActionType['ADD_TOAST']
- toast: ToasterToast
- }
- | {
- type: ActionType['UPDATE_TOAST']
- toast: Partial
- }
- | {
- type: ActionType['DISMISS_TOAST']
- toastId?: ToasterToast['id']
- }
- | {
- type: ActionType['REMOVE_TOAST']
- toastId?: ToasterToast['id']
- }
-
-interface State {
- toasts: ToasterToast[]
-}
-
-const toastTimeouts = new Map>()
-
-const addToRemoveQueue = (toastId: string) => {
- if (toastTimeouts.has(toastId)) {
- return
- }
-
- const timeout = setTimeout(() => {
- toastTimeouts.delete(toastId)
- dispatch({
- type: 'REMOVE_TOAST',
- toastId: toastId,
- })
- }, TOAST_REMOVE_DELAY)
-
- toastTimeouts.set(toastId, timeout)
-}
-
-export const reducer = (state: State, action: Action): State => {
- switch (action.type) {
- case 'ADD_TOAST':
- return {
- ...state,
- toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
- }
-
- case 'UPDATE_TOAST':
- return {
- ...state,
- toasts: state.toasts.map(t =>
- t.id === action.toast.id ? { ...t, ...action.toast } : t,
- ),
- }
-
- case 'DISMISS_TOAST': {
- const { toastId } = action
-
- // ! Side effects ! - This could be extracted into a dismissToast() action,
- // but I'll keep it here for simplicity
- if (toastId) {
- addToRemoveQueue(toastId)
- } else {
- state.toasts.forEach(toast => {
- addToRemoveQueue(toast.id)
- })
- }
-
- return {
- ...state,
- toasts: state.toasts.map(t =>
- t.id === toastId || toastId === undefined
- ? {
- ...t,
- open: false,
- }
- : t,
- ),
- }
- }
- case 'REMOVE_TOAST':
- if (action.toastId === undefined) {
- return {
- ...state,
- toasts: [],
- }
- }
- return {
- ...state,
- toasts: state.toasts.filter(t => t.id !== action.toastId),
- }
- }
-}
-
-const listeners: Array<(state: State) => void> = []
-
-let memoryState: State = { toasts: [] }
-
-function dispatch(action: Action) {
- memoryState = reducer(memoryState, action)
- listeners.forEach(listener => {
- listener(memoryState)
- })
-}
-
-type Toast = Omit
-
-function toast({ ...props }: Toast) {
- const id = genId()
-
- const update = (props: ToasterToast) =>
- dispatch({
- type: 'UPDATE_TOAST',
- toast: { ...props, id },
- })
- const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
-
- dispatch({
- type: 'ADD_TOAST',
- toast: {
- ...props,
- id,
- open: true,
- onOpenChange: open => {
- if (!open) dismiss()
- },
- },
- })
-
- return {
- id: id,
- dismiss,
- update,
- }
-}
-
-function useToast() {
- const [state, setState] = React.useState(memoryState)
-
- React.useEffect(() => {
- listeners.push(setState)
- return () => {
- const index = listeners.indexOf(setState)
- if (index > -1) {
- listeners.splice(index, 1)
- }
- }
- }, [state])
-
- return {
- ...state,
- toast,
- dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
- }
-}
-
-export { useToast, toast }
diff --git a/heat-stack/app/entry.client.tsx b/heat-stack/app/entry.client.tsx
index 59481246..cfea5580 100644
--- a/heat-stack/app/entry.client.tsx
+++ b/heat-stack/app/entry.client.tsx
@@ -2,21 +2,10 @@ import { RemixBrowser } from '@remix-run/react'
import { startTransition } from 'react'
import { hydrateRoot } from 'react-dom/client'
-if (ENV.MODE === 'development') {
- import('~/utils/devtools.tsx').then(({ init }) => init())
-}
if (ENV.MODE === 'production' && ENV.SENTRY_DSN) {
- import('~/utils/monitoring.client.tsx').then(({ init }) => init())
-}
-if (process.env.NODE_ENV === 'development') {
- import('remix-development-tools').then(({ initRouteBoundariesClient }) => {
- initRouteBoundariesClient()
- startTransition(() => {
- hydrateRoot(document, )
- })
- })
-} else {
- startTransition(() => {
- hydrateRoot(document, )
- })
+ import('./utils/monitoring.client.tsx').then(({ init }) => init())
}
+
+startTransition(() => {
+ hydrateRoot(document, )
+})
diff --git a/heat-stack/app/entry.server.tsx b/heat-stack/app/entry.server.tsx
index 570139e9..2d8e268b 100644
--- a/heat-stack/app/entry.server.tsx
+++ b/heat-stack/app/entry.server.tsx
@@ -1,9 +1,12 @@
-import { Response, type HandleDocumentRequestFunction } from '@remix-run/node'
+import { PassThrough } from 'stream'
+import {
+ createReadableStreamFromReadable,
+ type HandleDocumentRequestFunction,
+} from '@remix-run/node'
import { RemixServer } from '@remix-run/react'
import isbot from 'isbot'
import { getInstanceInfo } from 'litefs-js'
import { renderToPipeableStream } from 'react-dom/server'
-import { PassThrough } from 'stream'
import { getEnv, init } from './utils/env.server.ts'
import { NonceProvider } from './utils/nonce-provider.ts'
import { makeTimings } from './utils/timing.server.ts'
@@ -14,7 +17,7 @@ init()
global.ENV = getEnv()
if (ENV.MODE === 'production' && ENV.SENTRY_DSN) {
- import('~/utils/monitoring.server.ts').then(({ init }) => init())
+ import('./utils/monitoring.server.ts').then(({ init }) => init())
}
type DocRequestArgs = Parameters
@@ -27,13 +30,6 @@ export default async function handleRequest(...args: DocRequestArgs) {
remixContext,
loadContext,
] = args
- const context =
- process.env.NODE_ENV === 'development'
- ? await import('remix-development-tools').then(
- ({ initRouteBoundariesServer }) =>
- initRouteBoundariesServer(remixContext),
- )
- : remixContext
const { currentInstance, primaryInstance } = await getInstanceInfo()
responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
@@ -45,16 +41,15 @@ export default async function handleRequest(...args: DocRequestArgs) {
: 'onShellReady'
const nonce = String(loadContext.cspNonce) ?? undefined
- return new Promise((resolve, reject) => {
+ return new Promise(async (resolve, reject) => {
let didError = false
-
// NOTE: this timing will only include things that are rendered in the shell
// and will not include suspended components and deferred loaders
const timings = makeTimings('render', 'renderToPipeableStream')
const { pipe, abort } = renderToPipeableStream(
-
+
,
{
[callbackName]: () => {
@@ -62,7 +57,7 @@ export default async function handleRequest(...args: DocRequestArgs) {
responseHeaders.set('Content-Type', 'text/html')
responseHeaders.append('Server-Timing', timings.toString())
resolve(
- new Response(body, {
+ new Response(createReadableStreamFromReadable(body), {
headers: responseHeaders,
status: didError ? 500 : responseStatusCode,
}),
diff --git a/heat-stack/app/root.tsx b/heat-stack/app/root.tsx
index 176a77b1..a30fedc4 100644
--- a/heat-stack/app/root.tsx
+++ b/heat-stack/app/root.tsx
@@ -1,10 +1,12 @@
+import { useForm } from '@conform-to/react'
+import { parse } from '@conform-to/zod'
import { cssBundleHref } from '@remix-run/css-bundle'
import {
json,
type DataFunctionArgs,
type HeadersFunction,
type LinksFunction,
- type V2_MetaFunction,
+ type MetaFunction,
} from '@remix-run/node'
import {
Form,
@@ -15,15 +17,20 @@ import {
Outlet,
Scripts,
ScrollRestoration,
+ useFetcher,
+ useFetchers,
useLoaderData,
useMatches,
useSubmit,
} from '@remix-run/react'
import { withSentry } from '@sentry/remix'
import { Suspense, lazy, useEffect, useRef, useState } from 'react'
+import { z } from 'zod'
import { Confetti } from './components/confetti.tsx'
import { GeneralErrorBoundary } from './components/error-boundary.tsx'
+import { ErrorList } from './components/forms.tsx'
import { SearchBar } from './components/search-bar.tsx'
+import { EpicToaster } from './components/toaster.tsx'
import { Button } from './components/ui/button.tsx'
import {
DropdownMenu,
@@ -33,26 +40,27 @@ import {
DropdownMenuTrigger,
} from './components/ui/dropdown-menu.tsx'
import { Icon, href as iconsHref } from './components/ui/icon.tsx'
-import { Toaster } from './components/ui/toaster.tsx'
-import { ThemeSwitch, useTheme } from './routes/resources+/theme/index.tsx'
-import { getTheme } from './routes/resources+/theme/theme.server.ts'
-import fontStylestylesheetUrl from './styles/font.css'
-import tailwindStylesheetUrl from './styles/tailwind.css'
+import fontStyleSheetUrl from './styles/font.css'
+import tailwindStyleSheetUrl from './styles/tailwind.css'
import { authenticator, getUserId } from './utils/auth.server.ts'
-import { ClientHintCheck, getHints } from './utils/client-hints.tsx'
+import { ClientHintCheck, getHints, useHints } from './utils/client-hints.tsx'
+import { getConfetti } from './utils/confetti.server.ts'
import { prisma } from './utils/db.server.ts'
import { getEnv } from './utils/env.server.ts'
-import { getFlashSession } from './utils/flash-session.server.ts'
-import { combineHeaders, getDomainUrl, getUserImgSrc } from './utils/misc.tsx'
+import {
+ combineHeaders,
+ getDomainUrl,
+ getUserImgSrc,
+ invariantResponse,
+} from './utils/misc.tsx'
import { useNonce } from './utils/nonce-provider.ts'
+import { useRequestInfo } from './utils/request-info.ts'
+import { type Theme, setTheme, getTheme } from './utils/theme.server.ts'
import { makeTimings, time } from './utils/timing.server.ts'
-import { useToast } from './utils/useToast.tsx'
+import { getToast } from './utils/toast.server.ts'
import { useOptionalUser, useUser } from './utils/user.ts'
-import rdtStylesheetUrl from 'remix-development-tools/stylesheet.css'
-const RemixDevTools =
- process.env.NODE_ENV === 'development'
- ? lazy(() => import('remix-development-tools'))
- : undefined
+
+// import { WeatherExample } from './components/WeatherExample.tsx'
import * as pyodideModule from 'pyodide'
import engine from '../../rules-engine/src/rules_engine/engine.py';
@@ -70,20 +78,17 @@ const runPythonScript = async () => {
await pyodide.loadPackage("numpy")
await pyodide.runPythonAsync(engine);
return pyodide;
- };
-
+};
+
export const links: LinksFunction = () => {
return [
// Preload svg sprite as a resource to avoid render blocking
{ rel: 'preload', href: iconsHref, as: 'image' },
// Preload CSS as a resource to avoid render blocking
- { rel: 'preload', href: fontStylestylesheetUrl, as: 'style' },
- { rel: 'preload', href: tailwindStylesheetUrl, as: 'style' },
+ { rel: 'preload', href: fontStyleSheetUrl, as: 'style' },
+ { rel: 'preload', href: tailwindStyleSheetUrl, as: 'style' },
cssBundleHref ? { rel: 'preload', href: cssBundleHref, as: 'style' } : null,
- rdtStylesheetUrl && process.env.NODE_ENV === 'development'
- ? { rel: 'preload', href: rdtStylesheetUrl, as: 'style' }
- : null,
{ rel: 'mask-icon', href: '/favicons/mask-icon.svg' },
{
rel: 'alternate icon',
@@ -98,16 +103,13 @@ export const links: LinksFunction = () => {
} as const, // necessary to make typescript happy
//These should match the css preloads above to avoid css as render blocking resource
{ rel: 'icon', type: 'image/svg+xml', href: '/favicons/favicon.svg' },
- { rel: 'stylesheet', href: fontStylestylesheetUrl },
- { rel: 'stylesheet', href: tailwindStylesheetUrl },
+ { rel: 'stylesheet', href: fontStyleSheetUrl },
+ { rel: 'stylesheet', href: tailwindStyleSheetUrl },
cssBundleHref ? { rel: 'stylesheet', href: cssBundleHref } : null,
- rdtStylesheetUrl && process.env.NODE_ENV === 'development'
- ? { rel: 'stylesheet', href: rdtStylesheetUrl }
- : null,
].filter(Boolean)
}
-export const meta: V2_MetaFunction = ({ data }) => {
+export const meta: MetaFunction = ({ data }) => {
return [
{ title: data ? 'Epic Notes' : 'Error | Epic Notes' },
{ name: 'description', content: `Your own captain's log` },
@@ -125,9 +127,22 @@ export async function loader({ request }: DataFunctionArgs) {
const user = userId
? await time(
() =>
- prisma.user.findUnique({
+ prisma.user.findUniqueOrThrow({
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ image: { select: { id: true } },
+ roles: {
+ select: {
+ name: true,
+ permissions: {
+ select: { entity: true, action: true, access: true },
+ },
+ },
+ },
+ },
where: { id: userId },
- select: { id: true, name: true, username: true, imageId: true },
}),
{ timings, type: 'find user', desc: 'find user in root' },
)
@@ -138,7 +153,8 @@ export async function loader({ request }: DataFunctionArgs) {
// them in the database. Maybe they were deleted? Let's log them out.
await authenticator.logout(request, { redirectTo: '/' })
}
- const { flash, headers: flashHeaders } = await getFlashSession(request)
+ const { toast, headers: toastHeaders } = await getToast(request)
+ const { confettiId, headers: confettiHeaders } = getConfetti(request)
return json(
{
@@ -152,12 +168,14 @@ export async function loader({ request }: DataFunctionArgs) {
},
},
ENV: getEnv(),
- flash,
+ toast,
+ confettiId,
},
{
headers: combineHeaders(
{ 'Server-Timing': timings.toString() },
- flashHeaders,
+ toastHeaders,
+ confettiHeaders,
),
},
)
@@ -170,6 +188,34 @@ export const headers: HeadersFunction = ({ loaderHeaders }) => {
return headers
}
+const ThemeFormSchema = z.object({
+ theme: z.enum(['system', 'light', 'dark']),
+})
+
+export async function action({ request }: DataFunctionArgs) {
+ const formData = await request.formData()
+ invariantResponse(
+ formData.get('intent') === 'update-theme',
+ 'Invalid intent',
+ { status: 400 },
+ )
+ const submission = parse(formData, {
+ schema: ThemeFormSchema,
+ })
+ if (submission.intent !== 'submit') {
+ return json({ status: 'success', submission } as const)
+ }
+ if (!submission.value) {
+ return json({ status: 'error', submission } as const, { status: 400 })
+ }
+ const { theme } = submission.value
+
+ const responseInit = {
+ headers: { 'set-cookie': setTheme(theme) },
+ }
+ return json({ success: true, submission }, responseInit)
+}
+
function Document({
children,
nonce,
@@ -178,7 +224,7 @@ function Document({
}: {
children: React.ReactNode
nonce: string
- theme?: 'dark' | 'light'
+ theme?: Theme
env?: Record
}) {
const [output, setOutput] = useState('(loading python...)');
@@ -206,6 +252,7 @@ function Document({
Output:
{output}
+ {/* */}
{children}