diff --git a/.github/workflows/heat-stack.yml b/.github/workflows/heat-stack.yml
index 140bfd2e..648b2112 100644
--- a/.github/workflows/heat-stack.yml
+++ b/.github/workflows/heat-stack.yml
@@ -24,7 +24,7 @@ permissions:
jobs:
lint:
name: ⬣ ESLint
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
@@ -50,7 +50,7 @@ jobs:
typecheck:
name: ʦ TypeScript
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
@@ -73,7 +73,7 @@ jobs:
vitest:
name: ⚡ Vitest pyodide.test.ts
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
@@ -100,7 +100,7 @@ jobs:
# playwright tests work great but slight jank/inconsistency passing, and not used yet, so disabling for now
# playwright:
# name: 🎭 Playwright
- # runs-on: ubuntu-latest
+ # runs-on: ubuntu-22.04
# timeout-minutes: 60
# steps:
# - name: ⬇️ Checkout repo
diff --git a/.gitignore b/.gitignore
index 5e16b5c7..ee899313 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,8 @@ node_modules
# Easy way to create temporary files/folders that won't accidentally be added to git
*.local.*
+
+#local temporary folders
+heat-app
+venv
+heat-tmp
\ No newline at end of file
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 00000000..7619ac2b
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,11 @@
+{
+ "recommendations": [
+ "bradlc.vscode-tailwindcss",
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode",
+ "prisma.prisma",
+ "qwtel.sqlite-viewer",
+ "yoavbls.pretty-ts-errors",
+ "github.vscode-github-actions"
+ ]
+}
diff --git a/heat-stack/.env.example b/heat-stack/.env.example
index d117323c..46659864 100644
--- a/heat-stack/.env.example
+++ b/heat-stack/.env.example
@@ -3,6 +3,7 @@ DATABASE_PATH="./prisma/data.db"
DATABASE_URL="file:./data.db?connection_limit=1"
CACHE_DATABASE_PATH="./other/cache.db"
SESSION_SECRET="super-duper-s3cret"
+HONEYPOT_SECRET="super-duper-s3cret"
INTERNAL_COMMAND_TOKEN="some-made-up-token"
RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh"
SENTRY_DSN="your-dsn"
diff --git a/heat-stack/README.md b/heat-stack/README.md
index a63d17af..60a8605b 100644
--- a/heat-stack/README.md
+++ b/heat-stack/README.md
@@ -17,6 +17,7 @@ npm install
npm run dev
```
+
### Set up in a new GitHub CodingSpace:
```
@@ -30,6 +31,9 @@ npm run dev
If you have the node version manager (`nvm`), then `nvm use 18` avoids engine error with node v19+ or newer which is default. nvm is preinstalled in coding spaces.
+
+In Coding Spaces VSCode always go to hamburger menu -> File-> untick AutoSave. For a pic, see https://stackoverflow.com/a/76659316/14144258
+
### Under special circumstances:
Assume you don't need to, but if the version of pyodide changes run:
diff --git a/heat-stack/app/components/confetti.tsx b/heat-stack/app/components/confetti.tsx
deleted file mode 100644
index 69fbecef..00000000
--- a/heat-stack/app/components/confetti.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Index as ConfettiShower } from 'confetti-react'
-import { ClientOnly } from 'remix-utils'
-
-export function Confetti({ id }: { id?: string | null }) {
- if (!id) return null
-
- return (
-
- {() => (
-
- )}
-
- )
-}
diff --git a/heat-stack/app/components/error-boundary.tsx b/heat-stack/app/components/error-boundary.tsx
index 12175f31..80c1406b 100644
--- a/heat-stack/app/components/error-boundary.tsx
+++ b/heat-stack/app/components/error-boundary.tsx
@@ -1,9 +1,10 @@
import {
+ type ErrorResponse,
isRouteErrorResponse,
useParams,
useRouteError,
} from '@remix-run/react'
-import { type ErrorResponse } from '@remix-run/router'
+import { captureRemixErrorBoundaryError } from '@sentry/remix'
import { getErrorMessage } from '#app/utils/misc.tsx'
type StatusHandler = (info: {
@@ -25,6 +26,7 @@ export function GeneralErrorBoundary({
unexpectedErrorHandler?: (error: unknown) => JSX.Element | null
}) {
const error = useRouteError()
+ captureRemixErrorBoundaryError(error)
const params = useParams()
if (typeof document !== 'undefined') {
diff --git a/heat-stack/app/components/forms.tsx b/heat-stack/app/components/forms.tsx
index 0d362fcb..ad2868ad 100644
--- a/heat-stack/app/components/forms.tsx
+++ b/heat-stack/app/components/forms.tsx
@@ -19,7 +19,7 @@ export function ErrorList({
return (
{errorsToRender.map(e => (
- -
+
-
{e}
))}
@@ -64,7 +64,7 @@ export function TextareaField({
className,
}: {
labelProps: React.LabelHTMLAttributes
- textareaProps: React.InputHTMLAttributes
+ textareaProps: React.TextareaHTMLAttributes
errors?: ListOfErrors
className?: string
}) {
diff --git a/heat-stack/app/components/progress-bar.tsx b/heat-stack/app/components/progress-bar.tsx
new file mode 100644
index 00000000..f7f9b495
--- /dev/null
+++ b/heat-stack/app/components/progress-bar.tsx
@@ -0,0 +1,63 @@
+import { useNavigation } from '@remix-run/react'
+import { useEffect, useRef, useState } from 'react'
+import { useSpinDelay } from 'spin-delay'
+import { cn } from '#app/utils/misc.tsx'
+import { Icon } from './ui/icon.tsx'
+
+function EpicProgress() {
+ const transition = useNavigation()
+ const busy = transition.state !== 'idle'
+ const delayedPending = useSpinDelay(busy, {
+ delay: 600,
+ minDuration: 400,
+ })
+ const ref = useRef(null)
+ const [animationComplete, setAnimationComplete] = useState(true)
+
+ useEffect(() => {
+ if (!ref.current) return
+ if (delayedPending) setAnimationComplete(false)
+
+ const animationPromises = ref.current
+ .getAnimations()
+ .map(({ finished }) => finished)
+
+ Promise.allSettled(animationPromises).then(() => {
+ if (!delayedPending) setAnimationComplete(true)
+ })
+ }, [delayedPending])
+
+ return (
+
+
+ {delayedPending && (
+
+
+
+ )}
+
+ )
+}
+
+export { EpicProgress }
diff --git a/heat-stack/app/components/search-bar.tsx b/heat-stack/app/components/search-bar.tsx
index 859c52d6..84681927 100644
--- a/heat-stack/app/components/search-bar.tsx
+++ b/heat-stack/app/components/search-bar.tsx
@@ -1,4 +1,5 @@
import { Form, useSearchParams, useSubmit } from '@remix-run/react'
+import { useId } from 'react'
import { useDebounce, useIsPending } from '#app/utils/misc.tsx'
import { Icon } from './ui/icon.tsx'
import { Input } from './ui/input.tsx'
@@ -14,6 +15,7 @@ export function SearchBar({
autoFocus?: boolean
autoSubmit?: boolean
}) {
+ const id = useId()
const [searchParams] = useSearchParams()
const submit = useSubmit()
const isSubmitting = useIsPending({
@@ -33,13 +35,13 @@ export function SearchBar({
onChange={e => autoSubmit && handleFormChange(e.currentTarget)}
>
-