some cleanup
This commit is contained in:
@@ -1,48 +1,54 @@
|
||||
import { createSignal, For } from "solid-js";
|
||||
import { createSignal, For, onCleanup, onMount, Show } from "solid-js";
|
||||
import { createRouter, Todo } from "@<@var(context.project.name)>/rpc"
|
||||
import { createEffect } from 'solid-js'
|
||||
import "./app.css";
|
||||
|
||||
const router = createRouter("http://127.0.0.1:8080")
|
||||
// Changes made elsewhere (another tab, the mobile app) show up within this interval.
|
||||
const syncIntervalMs = 2000
|
||||
|
||||
export default function App() {
|
||||
const router = createRouter("http://127.0.0.1:8080")
|
||||
const [todos, setTodos] = createSignal<Todo[]>([]);
|
||||
const [offline, setOffline] = createSignal(false);
|
||||
const [todoToCreateTask, setTodoToCreateTask] = createSignal<string>("");
|
||||
const fetchTodos = () => {
|
||||
router.todos.listTodos({}).then((r) => {
|
||||
setTodos(r.todos)
|
||||
})
|
||||
setOffline(false)
|
||||
// Polling returns the same list most of the time; keep the old array so <For> keeps its rows.
|
||||
if (JSON.stringify(todos()) !== JSON.stringify(r.todos)) setTodos(r.todos)
|
||||
}).catch(() => setOffline(true))
|
||||
}
|
||||
createEffect(() => {
|
||||
onMount(() => {
|
||||
fetchTodos()
|
||||
const interval = setInterval(fetchTodos, syncIntervalMs)
|
||||
onCleanup(() => clearInterval(interval))
|
||||
})
|
||||
|
||||
const setTodoDone = (id: string, task: string) => {
|
||||
return async (event : Event & { currentTarget: HTMLInputElement }) => {
|
||||
await router.todos.updateTodo({ todo: { id, task, done: event.currentTarget.checked } })
|
||||
fetchTodos()
|
||||
const setTodoDone = (id: string, task: string) => {
|
||||
return (event: Event & { currentTarget: HTMLInputElement }) => {
|
||||
router.todos.updateTodo({ todo: { id, task, done: event.currentTarget.checked } }).then(fetchTodos, () => setOffline(true))
|
||||
}
|
||||
}
|
||||
|
||||
const deleteTodo = (id: string) => {
|
||||
return async () => {
|
||||
await router.todos.deleteTodo({ todo: { id } })
|
||||
fetchTodos()
|
||||
return () => {
|
||||
router.todos.deleteTodo({ todo: { id } }).then(fetchTodos, () => setOffline(true))
|
||||
}
|
||||
}
|
||||
|
||||
const createTodo = () => {
|
||||
router.todos.createTodo({ todo: { id: crypto.randomUUID(), task: todoToCreateTask() } }).then((r) => {
|
||||
console.log(r)
|
||||
const task = todoToCreateTask().trim()
|
||||
if (!task) return
|
||||
router.todos.createTodo({ todo: { id: crypto.randomUUID(), task } }).then(() => {
|
||||
setTodoToCreateTask("")
|
||||
fetchTodos()
|
||||
}).catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
}, () => setOffline(true))
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Create Todo</p>
|
||||
<input value={todoToCreateTask()} onInput={(e) => setTodoToCreateTask(e.currentTarget.value)} type="text"/>
|
||||
<Show when={offline()}><p style={{ color: "#d32f2f" }}>Can't reach the API, retrying…</p></Show>
|
||||
<input value={todoToCreateTask()} onInput={(e) => setTodoToCreateTask(e.currentTarget.value)} type="text" placeholder="What needs to be done?"/>
|
||||
<button onclick={createTodo}> create </button>
|
||||
<For each={todos()}>
|
||||
{(item:Todo) =>
|
||||
|
||||
+22
-17
@@ -1,50 +1,55 @@
|
||||
<script lang="ts">
|
||||
import { createRouter, type Todo } from "@<@var(context.project.name)>/rpc"
|
||||
import type { ChangeEventHandler } from "svelte/elements";
|
||||
const router = createRouter("http://127.0.0.1:8080");
|
||||
// Changes made elsewhere (another tab, the mobile app) show up within this interval.
|
||||
const syncIntervalMs = 2000
|
||||
let todos = $state<Todo[]>([])
|
||||
let offline = $state(false)
|
||||
let todoToCreateTask = $state<string>("")
|
||||
const fetchTodos = () => {
|
||||
router.todos.listTodos({}).then((r) => {
|
||||
todos = r.todos;
|
||||
})
|
||||
offline = false
|
||||
// Polling returns the same list most of the time; skip re-rendering it.
|
||||
if (JSON.stringify($state.snapshot(todos)) !== JSON.stringify(r.todos)) todos = r.todos
|
||||
}).catch(() => offline = true)
|
||||
}
|
||||
$effect(() => {
|
||||
fetchTodos()
|
||||
const interval = setInterval(fetchTodos, syncIntervalMs)
|
||||
return () => clearInterval(interval)
|
||||
})
|
||||
|
||||
const setTodoDone = (id: string, task: string) => {
|
||||
return async (event : Event & { currentTarget: HTMLInputElement }) => {
|
||||
await router.todos.updateTodo({ todo: { id, task, done: event.currentTarget.checked } })
|
||||
fetchTodos()
|
||||
const setTodoDone = (id: string, task: string) => {
|
||||
return (event: Event & { currentTarget: HTMLInputElement }) => {
|
||||
router.todos.updateTodo({ todo: { id, task, done: event.currentTarget.checked } }).then(fetchTodos, () => offline = true)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteTodo = (id: string) => {
|
||||
return async () => {
|
||||
await router.todos.deleteTodo({ todo: { id } })
|
||||
fetchTodos()
|
||||
return () => {
|
||||
router.todos.deleteTodo({ todo: { id } }).then(fetchTodos, () => offline = true)
|
||||
}
|
||||
}
|
||||
|
||||
const createTodo = () => {
|
||||
router.todos.createTodo({ todo: { id: crypto.randomUUID(), task: todoToCreateTask } }).then((r) => {
|
||||
console.log(r)
|
||||
const task = todoToCreateTask.trim()
|
||||
if (!task) return
|
||||
router.todos.createTodo({ todo: { id: crypto.randomUUID(), task } }).then(() => {
|
||||
todoToCreateTask = ""
|
||||
fetchTodos()
|
||||
}).catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
}, () => offline = true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h1>Create Todo</h1>
|
||||
<input bind:value={todoToCreateTask} type="text"/>
|
||||
{#if offline}<p style="color: #d32f2f">Can't reach the API, retrying…</p>{/if}
|
||||
<input bind:value={todoToCreateTask} type="text" placeholder="What needs to be done?"/>
|
||||
<button onclick={createTodo}> create </button>
|
||||
{#each todos as todo (todo.id)}
|
||||
<div>
|
||||
{todo.task}
|
||||
<input type='checkbox' onchange={setTodoDone(todo.id || "",todo.task)}/>
|
||||
<input type='checkbox' checked={todo.done} onchange={setTodoDone(todo.id || "",todo.task)}/>
|
||||
<button onclick={deleteTodo(todo.id || "")}>delete</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Expo HAS CHANGED
|
||||
|
||||
Read the exact versioned docs at https://docs.expo.dev/versions/v56.0.0/ before writing any code.
|
||||
Read the exact versioned docs at https://docs.expo.dev/versions/v57.0.0/ before writing any code.
|
||||
|
||||
@@ -8,8 +8,10 @@ import * as Crypto from "expo-crypto"
|
||||
import { background, colorInvert, controlSize, padding as iosPadding, shapes } from "@expo/ui/swift-ui/modifiers";
|
||||
// No numeric width() on Android: @expo/ui 57 can't convert it natively and crashes.
|
||||
import { fillMaxWidth, padding, weight } from "@expo/ui/jetpack-compose/modifiers";
|
||||
// The Android emulator reaches the host via 10.0.2.2; the iOS simulator shares the host's network.
|
||||
const apiUrl = Platform.OS === "android" ? "http://10.0.2.2:8080" : "http://127.0.0.1:8080"
|
||||
// The iOS simulator shares the host's network; devenv runs `adb reverse` so Android devices do too.
|
||||
const router = createRouter("http://127.0.0.1:8080")
|
||||
// Changes made elsewhere (another device, the web app) show up within this interval.
|
||||
const syncIntervalMs = 2000
|
||||
|
||||
// SwiftUI clips a border to its corner radius, cutting off the corners, so on iOS the
|
||||
// border is an outer rounded fill showing 1pt around an inner one.
|
||||
@@ -38,38 +40,42 @@ function AnimatedRow({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
const router = createRouter(apiUrl)
|
||||
const [todos, setTodos] = useState<Array<Todo>>([])
|
||||
const [offline, setOffline] = useState(false)
|
||||
const todoToCreateTask = useNativeState<string>("")
|
||||
const colorScheme = useColorScheme()
|
||||
const fetchTodos = () => {
|
||||
router.todos.listTodos({}).then((r) => {
|
||||
setTodos(r.todos)
|
||||
})
|
||||
setOffline(false)
|
||||
// Polling returns the same list most of the time; keep the old array to skip re-rendering.
|
||||
setTodos((current) => JSON.stringify(current) === JSON.stringify(r.todos) ? current : r.todos)
|
||||
}).catch(() => setOffline(true))
|
||||
}
|
||||
useEffect(() => {
|
||||
fetchTodos();
|
||||
fetchTodos()
|
||||
const interval = setInterval(fetchTodos, syncIntervalMs)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const setTodoDone = (id: string, task: string) => {
|
||||
return (done: boolean) => {
|
||||
router.todos.updateTodo({ todo: { id, task, done } }).then(fetchTodos)
|
||||
router.todos.updateTodo({ todo: { id, task, done } }).then(fetchTodos, () => setOffline(true))
|
||||
}
|
||||
}
|
||||
|
||||
const deleteTodo = (id: string) => {
|
||||
return () => {
|
||||
router.todos.deleteTodo({ todo: { id } }).then(fetchTodos)
|
||||
router.todos.deleteTodo({ todo: { id } }).then(fetchTodos, () => setOffline(true))
|
||||
}
|
||||
}
|
||||
|
||||
const createTodo = () => {
|
||||
router.todos.createTodo({ todo: { id: Crypto.randomUUID(), task: todoToCreateTask.value } }).then((r) => {
|
||||
console.log(r)
|
||||
const task = todoToCreateTask.value.trim()
|
||||
if (!task) return
|
||||
router.todos.createTodo({ todo: { id: Crypto.randomUUID(), task } }).then(() => {
|
||||
todoToCreateTask.value = ""
|
||||
fetchTodos()
|
||||
}).catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
}, () => setOffline(true))
|
||||
}
|
||||
|
||||
const updateTodoToCreateTask = (task: string) => {
|
||||
@@ -81,6 +87,7 @@ export default function Index() {
|
||||
<Host matchContents={{ vertical: true }}>
|
||||
<Column alignment="center" modifiers={[fillMaxWidth()]}>
|
||||
<Text textStyle={{fontSize:30}}> Create Todo </Text>
|
||||
{offline && <Text textStyle={{ color: "#FF3B30" }}>Can't reach the API, retrying…</Text>}
|
||||
<TextInput
|
||||
value={todoToCreateTask}
|
||||
onChangeText={updateTodoToCreateTask}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[install]
|
||||
# React Native expects one flat node_modules; isolated installs keep a copy per peer-dependency set.
|
||||
linker = "hoisted"
|
||||
+40
-16
@@ -1,6 +1,11 @@
|
||||
{ pkgs, lib, config, inputs, ... }:
|
||||
<@if(eq(context.project.mobile,"expo"))>
|
||||
let
|
||||
avdName = "Pixel_5_API36";
|
||||
# Fixed console port, so scripts can address this emulator as emulator-5554 even when others run.
|
||||
emulatorSerial = "emulator-5554";
|
||||
# Apple's xcrun, for tools that need `xcrun simctl`, which the Nix SDK's xcrun lacks.
|
||||
systemXcrun = pkgs.runCommand "system-xcrun" { } "mkdir -p $out/bin && ln -s /usr/bin/xcrun $out/bin/xcrun";
|
||||
# Sets dev_dir to Xcode's developer dir. devenv points DEVELOPER_DIR at the Nix SDK,
|
||||
# which xcode-select would echo back, so it is unset for the lookup.
|
||||
xcodeDevDir = ''
|
||||
@@ -22,14 +27,14 @@ in
|
||||
pkgs.protoc-gen-connect-go
|
||||
pkgs.protoc-gen-es
|
||||
pkgs.cobra-cli
|
||||
];
|
||||
]<@if(eq(context.project.mobile,"expo"))> ++ lib.optionals pkgs.stdenv.isDarwin [ pkgs.cocoapods ]<@endif>;
|
||||
<@if(eq(context.project.mobile,"expo"))>
|
||||
android = {
|
||||
enable = true;
|
||||
emulator = {
|
||||
enable = true;
|
||||
};
|
||||
buildTools.version = ["34.0.0" "35.0.0" "36.0.0" ]; # add 36.0.0
|
||||
buildTools.version = [ "34.0.0" "35.0.0" "36.0.0" ];
|
||||
reactNative.enable = true;
|
||||
android-studio = {
|
||||
enable = false;
|
||||
@@ -43,12 +48,16 @@ in
|
||||
enterShell = lib.optionalString pkgs.stdenv.isLinux ''
|
||||
export LD_LIBRARY_PATH=$ANDROID_HOME/emulator/lib64:$LD_LIBRARY_PATH
|
||||
'' + lib.optionalString pkgs.stdenv.isDarwin ''
|
||||
ios-check || true
|
||||
# Re-check at most daily once the iOS requirements are met. SKIP_IOS_CHECK=1 turns this off.
|
||||
ios_check_ok="$DEVENV_STATE/ios-check-ok"
|
||||
if [ -z "''${SKIP_IOS_CHECK:-}" ] && [ -z "$(find "$ios_check_ok" -mtime -1 2>/dev/null)" ]; then
|
||||
if ios-check; then touch "$ios_check_ok"; else echo "Set SKIP_IOS_CHECK=1 to hide this." >&2; fi
|
||||
fi
|
||||
'';
|
||||
# Use a system image matching the host CPU (arm64 on Apple Silicon, x86_64 elsewhere).
|
||||
scripts.create-avd.exec = ''
|
||||
if ! avdmanager list avd -c | grep -qx "Pixel_5_API34"; then
|
||||
echo no | avdmanager create avd -n "Pixel_5_API34" -k "system-images;android-34;google_apis_playstore;${if pkgs.stdenv.hostPlatform.isAarch64 then "arm64-v8a" else "x86_64"}" -d "pixel_5"
|
||||
if ! avdmanager list avd -c | grep -qx "${avdName}"; then
|
||||
echo no | avdmanager create avd -n "${avdName}" -k "system-images;android-36;google_apis_playstore;${if pkgs.stdenv.hostPlatform.isAarch64 then "arm64-v8a" else "x86_64"}" -d "pixel_5"
|
||||
fi
|
||||
'';
|
||||
# macOS only: Nix can't provide Xcode, so report what the iOS build still needs.
|
||||
@@ -140,27 +149,42 @@ in
|
||||
exec = "create-avd";
|
||||
};
|
||||
emulator = {
|
||||
exec = "emulator -avd Pixel_5_API34";
|
||||
exec = "emulator -avd ${avdName} -port 5554";
|
||||
after = ["devenv:processes:createavd@completed"];
|
||||
};
|
||||
# One Metro bundler serves both apps, so iOS doesn't depend on the Android build.
|
||||
metro = {
|
||||
exec = lib.optionalString pkgs.stdenv.isDarwin ''
|
||||
# Expo's CLI calls `xcrun simctl` to talk to the iOS simulator.
|
||||
${xcodeDevDir}
|
||||
export DEVELOPER_DIR="$dev_dir" PATH="${systemXcrun}/bin:$PATH"
|
||||
'' + "exec bunx expo start --dev-client";
|
||||
cwd = "./apps/mobile";
|
||||
ready.http.get = { port = 8081; path = "/status"; };
|
||||
};
|
||||
expodev = {
|
||||
# Wait for the emulator to finish booting, otherwise expo tries to start a second copy of the AVD.
|
||||
exec = ''
|
||||
adb -e wait-for-device
|
||||
until [ "$(adb -e shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = 1 ]; do sleep 2; done
|
||||
bunx expo run:android
|
||||
# Wait for the emulator to boot, otherwise expo tries to start a second copy of the AVD.
|
||||
for _ in $(seq 150); do
|
||||
if [ "$(adb -s ${emulatorSerial} shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = 1 ]; then
|
||||
# Lets the emulator and USB-connected devices reach the API on the host at 127.0.0.1:8080.
|
||||
for device in $(adb devices | awk 'NR > 1 && $2 == "device" { print $1 }'); do
|
||||
adb -s "$device" reverse tcp:8080 tcp:8080
|
||||
done
|
||||
exec bunx expo run:android --no-bundler --device ${avdName}
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "The Android emulator didn't finish booting within 5 minutes, see the emulator log." >&2
|
||||
exit 1
|
||||
'';
|
||||
cwd = "./apps/mobile";
|
||||
after= ["devenv:processes:emulator@started"];
|
||||
restart = {
|
||||
on = "always";
|
||||
};
|
||||
after = [ "devenv:processes:emulator@started" "devenv:processes:metro" ];
|
||||
};
|
||||
# Reuses the Metro bundler started by expodev.
|
||||
expoios = lib.mkIf pkgs.stdenv.isDarwin {
|
||||
exec = "run-ios";
|
||||
cwd = "./apps/mobile";
|
||||
after = ["devenv:processes:expodev@started"];
|
||||
after = [ "devenv:processes:metro" ];
|
||||
};
|
||||
<@endif>
|
||||
sqlwatcher = {
|
||||
|
||||
@@ -9,5 +9,8 @@
|
||||
"scripts": {
|
||||
"dev": "bun run --filter '*' dev",
|
||||
"build": "bun run --filter '*' build"
|
||||
}
|
||||
}<@if(eq(context.project.mobile,"expo"))>,
|
||||
"overrides": {
|
||||
"react-native-screens": "~4.26.0"
|
||||
}<@endif>
|
||||
}
|
||||
|
||||
@@ -32,9 +32,7 @@ var serveCmd = &cobra.Command{
|
||||
}
|
||||
conf := *config.Conf
|
||||
ctx := context.Background()
|
||||
db_url := "postgres://" + conf.Database.User + ":" + conf.Database.Password + "@" + conf.Database.Host + ":" + strconv.Itoa(conf.Database.Port) + "/" + conf.Database.Name
|
||||
db_url = db_url + "?sslmode=disable"
|
||||
conn, err := pgxpool.New(ctx, db_url)
|
||||
conn, err := pgxpool.New(ctx, conf.Database.URL().String())
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ package config
|
||||
import (
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/pelletier/go-toml"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var confPath string
|
||||
@@ -18,7 +21,7 @@ func init() {
|
||||
Server: Server{
|
||||
Host: "127.0.0.1",
|
||||
Port: 8080,
|
||||
FrontendUrls: []string{"http://localhost:5173"},
|
||||
FrontendUrls: []string{"http://localhost:5173", "http://127.0.0.1:5173"},
|
||||
},
|
||||
Database: Database{
|
||||
User: "pp",
|
||||
@@ -75,6 +78,17 @@ type Database struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// URL returns the Postgres connection URL. The local dev database has no SSL.
|
||||
func (d Database) URL() *url.URL {
|
||||
return &url.URL{
|
||||
Scheme: "postgres",
|
||||
User: url.UserPassword(d.User, d.Password),
|
||||
Host: net.JoinHostPort(d.Host, strconv.Itoa(d.Port)),
|
||||
Path: "/" + d.Name,
|
||||
RawQuery: "sslmode=disable",
|
||||
}
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Server Server
|
||||
Database Database
|
||||
|
||||
@@ -5,9 +5,6 @@ import (
|
||||
"github.com/amacneil/dbmate/v2/pkg/dbmate"
|
||||
_ "github.com/amacneil/dbmate/v2/pkg/driver/postgres"
|
||||
"log"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var Q *Queries
|
||||
@@ -19,23 +16,7 @@ func RunDbMigrations() error {
|
||||
return err
|
||||
}
|
||||
conf := *config.Conf
|
||||
dbUrl, err := url.Parse(strings.Join([]string{
|
||||
"postgresql://",
|
||||
conf.Database.User,
|
||||
":",
|
||||
conf.Database.Password,
|
||||
"@",
|
||||
conf.Database.Host,
|
||||
":",
|
||||
strconv.Itoa(conf.Database.Port),
|
||||
"/",
|
||||
conf.Database.Name,
|
||||
"?sslmode=disable",
|
||||
}, ""))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db := dbmate.New(dbUrl)
|
||||
db := dbmate.New(conf.Database.URL())
|
||||
db.FS = MigrationsFs
|
||||
db.MigrationsDir = []string{"migrations"}
|
||||
migrations, err := db.FindMigrations()
|
||||
|
||||
Reference in New Issue
Block a user