Compare commits

5 Commits

Author SHA1 Message Date
15565b7208 always restart expo dev 2026-06-03 17:07:34 +02:00
d1c46cdee1 add expo, massively simplify svelte and solid examples 2026-06-03 17:05:06 +02:00
d33b9c5467 devenv tweaks 2026-06-02 12:34:32 +02:00
df0e78d9ba mobile -> string 2026-06-02 12:03:42 +02:00
cc596e632f expo template 2026-06-02 12:01:39 +02:00
64 changed files with 633 additions and 463 deletions

View File

@@ -22,9 +22,18 @@ const project = await p.group(
await p.select({ await p.select({
message: 'Pick a frontend framework.', message: 'Pick a frontend framework.',
options: [ options: [
{value: "svelte-kit", label:"SvelteKit"}, { value: "svelte-kit", label: "SvelteKit" },
{value: "solid-start", label:"SolidStart"}, { value: "solid-start", label: "SolidStart" },
{value: "none", label:"None"} { value: "none", label: "None" }
]
})
,
mobile: async () =>
await p.select({
message: 'Pick a mobile framework.',
options: [
{ value: "expo", label: "ReactNative + Expo" },
{ value: "none", label: "None" }
] ]
}) })
, ,
@@ -37,6 +46,9 @@ const project = await p.group(
return undefined return undefined
}, },
}), }),
installDeps: async () => {
return await p.confirm({message: "Install dependencies?", })
}
}, },
{ {
onCancel: () => { onCancel: () => {
@@ -53,18 +65,17 @@ const render = createRenderer(z.object({
name: z.string(), name: z.string(),
goprefix: z.string(), goprefix: z.string(),
frontend: z.string(), frontend: z.string(),
mobile: z.string()
}) })
})) }))
const destDir = path.join("./",project.name); const destDir = path.join("./", project.name);
render(destDir,{project},{reverseMap: true}) render(destDir, { project }, { reverseMap: true })
const s = p.spinner(); const s = p.spinner();
s.start("Installing dependencies"); if (project.installDeps) {
await Bun.$`bun install`.cwd(path.join(destDir)).quiet(); s.start("Installing dependencies");
await Bun.$`bun install`.cwd(path.join(destDir,'packages','rpc')).quiet(); await Bun.$`bun install`.cwd(path.join(destDir)).quiet();
if (project.frontend !== "none") { s.stop("Dependencies installed.");
await Bun.$`bun install`.cwd(path.join(destDir,'apps','web')).quiet();
} }
s.stop("Dependencies installed.");
p.outro("You're all set!"); p.outro("You're all set!");

View File

@@ -1,5 +1,5 @@
{ {
"name": "example-basic", "name": "example-bare",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@@ -9,16 +9,9 @@
}, },
"dependencies": { "dependencies": {
"@<@var(context.project.name)>/rpc": "workspace:*", "@<@var(context.project.name)>/rpc": "workspace:*",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0",
"@solidjs/start": "2.0.0-alpha.2", "@solidjs/start": "2.0.0-alpha.2",
"@solidjs/vite-plugin-nitro-2": "^0.1.0", "@solidjs/vite-plugin-nitro-2": "^0.1.0",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/query-db-collection": "^1.0.35",
"@tanstack/solid-db": "^0.2.18",
"@tanstack/solid-query": "^5.96.2",
"solid-js": "^1.9.5", "solid-js": "^1.9.5",
"tailwindcss": "^4.2.2",
"vite": "^7.0.0" "vite": "^7.0.0"
}, },
"engines": { "engines": {

View File

@@ -1,5 +1,61 @@
@import "tailwindcss";
body { body {
font-family: Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; font-family: Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
} }
a {
margin-right: 1rem;
}
main {
text-align: center;
padding: 1em;
margin: 0 auto;
}
h1 {
color: #335d92;
text-transform: uppercase;
font-size: 4rem;
font-weight: 100;
line-height: 1.1;
margin: 4rem auto;
max-width: 14rem;
}
p {
max-width: 14rem;
margin: 2rem auto;
line-height: 1.35;
}
@media (min-width: 480px) {
h1 {
max-width: none;
}
p {
max-width: none;
}
}
.increment {
font-family: inherit;
font-size: inherit;
padding: 1em 2em;
color: #335d92;
background-color: rgba(68, 107, 158, 0.1);
border-radius: 2em;
border: 2px solid rgba(68, 107, 158, 0);
outline: none;
width: 200px;
font-variant-numeric: tabular-nums;
cursor: pointer;
}
.increment:focus {
border: 2px solid #335d92;
}
.increment:active {
background-color: rgba(68, 107, 158, 0.2);
}

View File

@@ -1,58 +1,58 @@
import { MetaProvider, Title } from "@solidjs/meta"; import { createSignal, For } from "solid-js";
import { Router } from "@solidjs/router"; import { createRouter, Todo } from "@<@var(context.project.name)>/rpc"
import { FileRoutes } from "@solidjs/start/router"; import { createEffect } from 'solid-js'
import { Suspense } from "solid-js";
import { createCollection } from '@tanstack/solid-db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'
import type { Todo, ExtractPayload } from '@<@var(context.project.name)>/rpc'
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
import { getRouter } from "./lib/getRouter"
import "./app.css"; import "./app.css";
import { TodoCollectionProvider } from "./context/todocollection/provider";
const queryClient = new QueryClient({
})
const router = getRouter();
export default function App() { export default function App() {
const todosCollection = createCollection( const router = createRouter("http://127.0.0.1:8080")
queryCollectionOptions({ const [todos, setTodos] = createSignal<Todo[]>([]);
queryKey: ["todos"], const [todoToCreateTask, setTodoToCreateTask] = createSignal<string>("");
queryFn: async () => { const fetchTodos = () => {
const todos = await router.todos.listTodos({}) router.todos.listTodos({}).then((r) => {
return todos.todos as ExtractPayload<Todo>[]; setTodos(r.todos)
},
queryClient,
getKey: (item) => item.id ? item.id : crypto.randomUUID(),
onInsert: async ({ transaction }) => {
Promise.all(transaction.mutations.map((m) => {
router.todos.createTodo({ todo: m.modified })
}))
},
onDelete: async ({ transaction }) => {
Promise.all(transaction.mutations.map((m) => {
router.todos.deleteTodo({ todo: m.modified })
}))
},
onUpdate: async ({ transaction }) => {
Promise.all(transaction.mutations.map((m) => {
router.todos.updateTodo({ todo: m.modified })
}))
}
}) })
) }
createEffect(() => {
fetchTodos()
})
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 deleteTodo = (id: string) => {
return async () => {
await router.todos.deleteTodo({ todo: { id } })
fetchTodos()
}
}
const createTodo = () => {
router.todos.createTodo({ todo: { id: crypto.randomUUID(), task: todoToCreateTask() } }).then((r) => {
console.log(r)
fetchTodos()
}).catch((e) => {
console.log(e)
})
}
return ( return (
<Router <div>
root={props => ( <p>Create Todo</p>
<MetaProvider> <input value={todoToCreateTask()} onInput={(e) => setTodoToCreateTask(e.currentTarget.value)} type="text"/>
<QueryClientProvider client={queryClient} > <button onclick={createTodo}> create </button>
<TodoCollectionProvider value={todosCollection}> <For each={todos()}>
<Suspense>{props.children}</Suspense> {(item:Todo) =>
</TodoCollectionProvider> <div>
</QueryClientProvider> {item.task}
</MetaProvider> <input type='checkbox' checked={item.done} onChange={setTodoDone(item.id || "",item.task)}/>
)} <button onclick={deleteTodo(item.id || "")}>delete</button>
> </div>
<FileRoutes/> }
</Router> </For>
</div>
); );
} }

View File

@@ -1,38 +0,0 @@
import { ExtractPayload, type Todo } from '@<@var(context.project.name)>/rpc'
import { createSignal } from 'solid-js';
import { useTodoCollection } from '~/context/todocollection/create';
export const CreateTodo = () => {
const [todoState, setTodoState] = createSignal<ExtractPayload<Todo>>({
id: crypto.randomUUID(),
task: "",
done: false,
})
let inputRef!: HTMLInputElement
const setTask = () => {
const todo = {...todoState()}
todo.task = inputRef.value;
setTodoState(todo)
}
const todoCollection = useTodoCollection()
const insertTodo = () => {
todoCollection.insert(todoState())
setTodoState({
id: crypto.randomUUID(),
task: "",
done: false
})
inputRef.value = "";
}
return (
<>
<input class="border rounded-md px-3 py-1" ref={inputRef} type='text' onInput={setTask} />
<button class="bg-teal-800 text-teal-100 p-1 rounded-md cursor-pointer" onClick={insertTodo}> create </button>
</>
)
}

View File

@@ -1,29 +0,0 @@
import { useLiveQuery } from "@tanstack/solid-db";
import { useTodoCollection } from "~/context/todocollection/create";
import { Todo } from './Todo'
import { For } from 'solid-js'
export const ListTodos = () => {
const todoCollection = useTodoCollection()
const data = useLiveQuery((q) =>
q.from({todos: todoCollection})
)
const todos = () => {
const items = Array.from(data.state.values());
return items.sort((a: any, b: any) => {
if (a.done != b.done) return a.done ? -1 : 1
if (!a.done && !b.done) {
const adate = a.createdAt ? new Date(a.createdAt) : new Date()
const bdate = b.createdAt ? new Date(b.createdAt) : new Date()
return bdate.getTime() - adate.getTime()
}
return 0
})
}
return (
<For each={todos()} fallback={<div>Loading...</div>}>
{(item) => <Todo todo={item} />}
</For>
)
}

View File

@@ -1,44 +0,0 @@
import { ExtractPayload, type Todo as RpcTodo } from '@<@var(context.project.name)>/rpc'
import { createSignal, type JSX } from 'solid-js';
import { useTodoCollection } from '~/context/todocollection/create';
export const Todo = ({ todo }: { todo: ExtractPayload<RpcTodo> }) => {
const [todoState, setTodoState] = createSignal(todo)
const todoCollection = useTodoCollection()
let commitUpdateTimeoutId: NodeJS.Timeout|null = null;
const updateTask: JSX.EventHandlerUnion<HTMLInputElement, Event> = (e) => {
if (commitUpdateTimeoutId != null) {
clearTimeout(commitUpdateTimeoutId)
}
const todo = { ...todoState() };
todo.task = e.currentTarget.value
setTodoState(todo)
commitUpdateTimeoutId = setTimeout(() => {
todoCollection.update(todoState().id, (draft) => {
draft.task = todoState().task
})
},3000)
}
const updateDone: JSX.EventHandlerUnion<HTMLInputElement, Event> = (e) => {
const todo = { ...todoState() }
todo.done = e.currentTarget.checked
setTodoState(todo)
todoCollection.update(todoState().id, (draft) => {
draft.done = todoState().done
})
}
const del = () => {
todoCollection.delete(todoState().id || "")
}
return (
<div class="flex flex-col items-center border rounded-md p-5 gap-2">
<input class="text-center border rounded-md px-3 py-1" oninput={updateTask} type="text" value={todoState().task} />
<span> {(new Date(todoState().createdAt || "")).toLocaleString()}</span>
<span> {(new Date(todoState().updatesAt || "")).toLocaleString()}</span>
<input type="checkbox" onchange={updateDone} checked={todoState().done} />
<button class="bg-amber-800 text-amber-100 p-1 rounded-md cursor-pointer" onclick={del} class=""> delete </button>
</div>
)
}

View File

@@ -1,12 +0,0 @@
import { createContext,useContext } from 'solid-js'
import { type CollectionImpl, type CollectionLike } from '@tanstack/solid-db'
import type { ExtractPayload, Todo } from '@<@var(context.project.name)>/rpc';
export type TodoCollection = CollectionImpl<ExtractPayload<Todo>>
export const TodoCollectionContext = createContext<TodoCollection>()
export const useTodoCollection = () => {
const col = useContext(TodoCollectionContext)
if (col === undefined) {
throw new Error("Todo collection has not been initialized")
}
return col;
}

View File

@@ -1,8 +0,0 @@
import { JSXElement } from 'solid-js';
import { TodoCollection, TodoCollectionContext } from './create'
export const TodoCollectionProvider = (props : {children:JSXElement, value: TodoCollection}) => {
return (
<TodoCollectionContext.Provider value={props.value}> {props.children} </TodoCollectionContext.Provider>
)
}

View File

@@ -1,7 +0,0 @@
import { createRouter } from "@<@var(context.project.name)>/rpc"
const router = createRouter("http://127.0.0.1:8080")
export const getRouter = () => {
return router;
}

View File

@@ -1,19 +0,0 @@
import { Title } from "@solidjs/meta";
import { HttpStatusCode } from "@solidjs/start";
export default function NotFound() {
return (
<main>
<Title>Not Found</Title>
<HttpStatusCode code={404} />
<h1>Page Not Found</h1>
<p>
Visit{" "}
<a href="https://start.solidjs.com" target="_blank">
start.solidjs.com
</a>{" "}
to learn how to build SolidStart apps.
</p>
</main>
);
}

View File

@@ -1,13 +0,0 @@
import { Title } from "@solidjs/meta";
import { CreateTodo } from "~/components/CreateTodo";
import { ListTodos } from "~/components/ListTodos"
export default function Home() {
return (
<main class="flex flex-col items-center gap-2 py-5">
<Title>Todos</Title>
<h1 class="text-5xl"> Todos </h1>
<CreateTodo/>
<ListTodos/>
</main>
);
}

View File

@@ -1,12 +1,10 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import { nitroV2Plugin as nitro } from "@solidjs/vite-plugin-nitro-2"; import { nitroV2Plugin as nitro } from "@solidjs/vite-plugin-nitro-2";
import tailwindcss from '@tailwindcss/vite'
import { solidStart } from "@solidjs/start/config"; import { solidStart } from "@solidjs/start/config";
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [solidStart(),
solidStart(), nitro()
nitro(),
tailwindcss()
] ]
}); });

View File

@@ -21,14 +21,6 @@
"vite": "^8.0.7" "vite": "^8.0.7"
}, },
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^2.11.0", "@<@var(context.project.name)>/rpc": "workspace:*"
"@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-web": "^2.1.1",
"@<@var(context.project.name)>/rpc": "workspace:*",
"@tailwindcss/vite": "^4.3.0",
"@tanstack/query-db-collection": "^1.0.33",
"@tanstack/svelte-db": "^0.1.79",
"@tanstack/svelte-query": "^6.1.13",
"tailwindcss": "^4.3.0"
} }
} }

View File

@@ -1,25 +0,0 @@
<script lang="ts">
import type { ExtractPayload, Todo } from '@<@var(context.project.name)>/rpc';
import { getTodoCollection } from '$lib/todocollectionscontext';
const todoCollection = getTodoCollection();
let todo = $state<ExtractPayload<Todo>>({
id: crypto.randomUUID(),
task: '',
done: false
});
const insertTodo = () => {
todoCollection.insert({ ...todo });
todo = {
id: crypto.randomUUID(),
task: '',
done: false
};
};
</script>
<input class="rounded-md border px-3 py-1" type="text" bind:value={todo.task} />
<button class="cursor-pointer rounded-md bg-teal-800 p-1 text-teal-100" onclick={insertTodo}>
create
</button>

View File

@@ -1,29 +0,0 @@
<script lang="ts">
import { getTodoCollection } from '$lib/todocollectionscontext';
import { useLiveQuery } from '@tanstack/svelte-db';
import Todo from './Todo.svelte';
const todoCollection = getTodoCollection();
const data = useLiveQuery((q) => q.from({ todos: todoCollection }));
const todos = $derived.by(() => {
return data.data.toSorted((a, b) => {
if (a.done != b.done) {
return a.done ? -1 : 1;
}
if (!a.done && !b.done) {
const adate = a.createdAt ? new Date(a.createdAt) : new Date();
const bdate = b.createdAt ? new Date(b.createdAt) : new Date();
return bdate.getTime() - adate.getTime();
}
return 0;
});
});
</script>
{#if data.isLoading}
<div>Loading...</div>
{:else}
{#each todos as todo (todo.id)}
<Todo {todo} />
{/each}
{/if}

View File

@@ -1,48 +0,0 @@
<script lang="ts">
import type { ExtractPayload, Todo as RpcTodo } from '@<@var(context.project.name)>/rpc';
import { getTodoCollection } from '$lib/todocollectionscontext';
let { todo }: { todo: ExtractPayload<RpcTodo> } = $props();
const getInitialTodo = () => todo;
let todoState = $state<ExtractPayload<RpcTodo>>({ ...getInitialTodo() });
let commitUpdateTimeoutId: ReturnType<typeof setTimeout> | null = null;
const todoCollection = getTodoCollection();
const updateTask = () => {
if (commitUpdateTimeoutId != null) {
clearTimeout(commitUpdateTimeoutId);
}
commitUpdateTimeoutId = setTimeout(() => {
todoCollection.update(todoState.id, (draft) => {
draft.task = todoState.task;
});
}, 3000);
};
const updateDone = () => {
todoCollection.update(todoState.id, (draft) => {
draft.done = todoState.done;
});
};
const del = () => {
todoCollection.delete(todoState.id || '');
};
</script>
<div class="flex flex-col items-center gap-2 rounded-md border p-5">
<input
class="rounded-md border px-3 py-1 text-center"
type="text"
bind:value={todoState.task}
oninput={updateTask}
/>
<span>{new Date(todoState.createdAt || '').toLocaleString()}</span>
<span>{new Date(todoState.updatesAt || '').toLocaleString()}</span>
<input type="checkbox" bind:checked={todoState.done} onchange={updateDone} />
<button class="cursor-pointer rounded-md bg-amber-800 p-1 text-amber-100" onclick={del}>
delete
</button>
</div>

View File

@@ -1,8 +0,0 @@
import {createRouter} from '@<@var(context.project.name)>/rpc'
const router = createRouter("http://127.0.0.1:8080")
export const getRouter = () => {
return router;
}

View File

@@ -1,4 +0,0 @@
import { createContext } from "svelte";
import { type CollectionImpl } from '@tanstack/svelte-db'
import type { Todo, ExtractPayload } from "@<@var(context.project.name)>/rpc";
export const [getTodoCollection,setTodoCollection] = createContext<CollectionImpl<ExtractPayload<Todo>,string>>()

View File

@@ -1,53 +1,11 @@
<script lang="ts"> <script lang="ts">
import { createCollection } from '@tanstack/svelte-db';
import { queryCollectionOptions } from '@tanstack/query-db-collection';
import { getRouter } from "$lib/getconnectrouter"
import favicon from '$lib/assets/favicon.svg'; import favicon from '$lib/assets/favicon.svg';
import "../app.css"
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
import type { ExtractPayload, Todo } from '@<@var(context.project.name)>/rpc';
import { browser } from '$app/environment';
import { setTodoCollection } from '$lib/todocollectionscontext';
let { children } = $props(); let { children } = $props();
const router = getRouter()
const queryClient = new QueryClient({
defaultOptions: {
queries: {
enabled: browser
}
},
})
const todosCollection = createCollection(
queryCollectionOptions({
queryKey: ["todos"],
queryFn: async () => {
const todos = await router.todos.listTodos({})
return todos.todos as ExtractPayload<Todo>[];
},
queryClient,
getKey: (item) => item.id ? item.id : crypto.randomUUID(),
onInsert: async ({transaction}) => {
Promise.all(transaction.mutations.map((m) => {
router.todos.createTodo({ todo: m.modified })
}))
},
onDelete: async ({transaction}) => {
Promise.all(transaction.mutations.map((m) => {
router.todos.deleteTodo({todo: m.modified})
}))
},
onUpdate: async ({transaction}) => {
Promise.all(transaction.mutations.map((m) => {
router.todos.updateTodo({todo: m.modified})
}))
}
})
)
setTodoCollection(todosCollection)
</script> </script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
<QueryClientProvider client={queryClient}> {@render children()}
{@render children()}
</QueryClientProvider>

View File

@@ -1,11 +1,51 @@
<script> <script lang="ts">
import CreateTodo from "$lib/components/CreateTodo.svelte"; import { createRouter, type Todo } from "@glstack-test/rpc"
import ListTodos from "$lib/components/ListTodos.svelte"; import type { ChangeEventHandler } from "svelte/elements";
const router = createRouter("http://127.0.0.1:8080");
let todos = $state<Todo[]>([])
let todoToCreateTask = $state<string>("")
const fetchTodos = () => {
router.todos.listTodos({}).then((r) => {
todos = r.todos;
})
}
$effect(() => {
fetchTodos()
})
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 deleteTodo = (id: string) => {
return async () => {
await router.todos.deleteTodo({ todo: { id } })
fetchTodos()
}
}
const createTodo = () => {
router.todos.createTodo({ todo: { id: crypto.randomUUID(), task: todoToCreateTask } }).then((r) => {
console.log(r)
fetchTodos()
}).catch((e) => {
console.log(e)
})
}
</script> </script>
<main class="flex flex-col items-center gap-2 py-5"> <div>
<title>Todos</title> <h1>Create Todo</h1>
<h1 class="text-5xl"> Todos </h1> <input bind:value={todoToCreateTask} type="text"/>
<CreateTodo/> <button onclick={createTodo}> create </button>
<ListTodos/> {#each todos as todo (todo.id)}
</main> <div>
{todo.task}
<input type='checkbox' onchange={setTodoDone(todo.id || "",todo.task)}/>
<button onclick={deleteTodo(todo.id || "")}>delete</button>
</div>
{/each}
</div>

View File

@@ -1,9 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [sveltekit()]
tailwindcss(),
sveltekit(),
]
}); });

View File

@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"expo@claude-plugins-official": true
}
}

View File

@@ -0,0 +1,43 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
example
# generated native folders
/ios
/android

View File

@@ -0,0 +1,4 @@
.expo
android
node_modules
assets

View File

@@ -0,0 +1 @@
{ "recommendations": ["expo.vscode-expo-tools"] }

View File

@@ -0,0 +1,7 @@
{
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit",
"source.sortMembers": "explicit"
}
}

View File

@@ -0,0 +1,3 @@
# Expo HAS CHANGED
Read the exact versioned docs at https://docs.expo.dev/versions/v56.0.0/ before writing any code.

View File

@@ -0,0 +1 @@
@AGENTS.md

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,45 @@
{
"expo": {
"name": "mobile",
"slug": "mobile",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "mobile",
"userInterfaceStyle": "automatic",
"ios": {
"icon": "./assets/expo.icon"
},
"android": {
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"predictiveBackGestureEnabled": false,
"package": "com.gregorl.mobile"
},
"web": {
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
[
"expo-splash-screen",
{
"backgroundColor": "#208AEF",
"android": {
"image": "./assets/images/splash-icon.png",
"imageWidth": 76
}
}
]
],
"experiments": {
"typedRoutes": true,
"reactCompiler": true
}
}
}

View File

@@ -0,0 +1,3 @@
<svg width="652" height="606" viewBox="0 0 652 606" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M353.554 0H298.446C273.006 0 249.684 14.6347 237.962 37.9539L4.37994 502.646C-1.04325 513.435 -1.45067 526.178 3.2716 537.313L22.6123 582.918C34.6475 611.297 72.5404 614.156 88.4414 587.885L309.863 222.063C313.34 216.317 319.439 212.826 326 212.826C332.561 212.826 338.659 216.317 342.137 222.063L563.559 587.885C579.46 614.156 617.352 611.297 629.388 582.918L648.728 537.313C653.451 526.178 653.043 513.435 647.62 502.646L414.038 37.9539C402.316 14.6347 378.994 0 353.554 0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -0,0 +1,40 @@
{
"fill" : {
"automatic-gradient" : "extended-srgb:0.00000,0.47843,1.00000,1.00000"
},
"groups" : [
{
"layers" : [
{
"image-name" : "expo-symbol 2.svg",
"name" : "expo-symbol 2",
"position" : {
"scale" : 1,
"translation-in-points" : [
1.1008400065293245e-05,
-16.046875
]
}
},
{
"image-name" : "grid.png",
"name" : "grid"
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1,19 @@
// Learn more: https://docs.expo.dev/guides/monorepos/
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');
const projectRoot = __dirname;
const monorepoRoot = path.resolve(projectRoot, '../..');
const config = getDefaultConfig(projectRoot);
// Watch the whole monorepo so changes in packages/* trigger rebuilds.
config.watchFolders = [monorepoRoot];
// Resolve modules from the app's node_modules first, then the workspace root.
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(monorepoRoot, 'node_modules'),
];
module.exports = config;

View File

@@ -0,0 +1,46 @@
{
"name": "mobile",
"main": "expo-router/entry",
"version": "1.0.0",
"dependencies": {
"@expo/ui": "~56.0.14",
"@<@var(context.project.name)>/rpc": "workspace:*",
"expo": "~56.0.5",
"expo-constants": "~56.0.16",
"expo-crypto": "~56.0.4",
"expo-dev-client": "~56.0.18",
"expo-device": "~56.0.4",
"expo-font": "~56.0.5",
"expo-glass-effect": "~56.0.4",
"expo-image": "~56.0.9",
"expo-linking": "~56.0.12",
"expo-router": "~56.2.7",
"expo-splash-screen": "~56.0.10",
"expo-status-bar": "~56.0.4",
"expo-symbols": "~56.0.5",
"expo-system-ui": "~56.0.5",
"expo-web-browser": "~56.0.5",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-native": "0.85.3",
"react-native-gesture-handler": "~2.31.1",
"react-native-reanimated": "4.3.1",
"react-native-safe-area-context": "~5.7.0",
"react-native-screens": "4.25.2",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.8.3"
},
"devDependencies": {
"@types/react": "~19.2.2",
"typescript": "~6.0.3"
},
"scripts": {
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"lint": "expo lint"
},
"private": true
}

View File

@@ -0,0 +1,5 @@
import { Stack } from "expo-router";
export default function RootLayout() {
return <Stack screenOptions={{headerShown:false}} />;
}

View File

@@ -0,0 +1,73 @@
import { createRouter, Todo } from "@<@var(context.project.name)>/rpc"
import { useEffect, useState } from "react"
import { ScrollView, Button, Checkbox, Host, Column, TextInput, Text, useNativeState, Row, Icon } from "@expo/ui";
import * as Crypto from "expo-crypto"
import { colorInvert, controlSize } from "@expo/ui/swift-ui/modifiers";
import { fillMaxWidth, padding, width } from "@expo/ui/jetpack-compose/modifiers";
export default function Index() {
const router = createRouter("http://10.0.2.2:8080")
const [todos, setTodos] = useState<Array<Todo>>([])
const todoToCreateTask = useNativeState<string>("")
const fetchTodos = () => {
router.todos.listTodos({}).then((r) => {
setTodos(r.todos)
})
}
useEffect(() => {
fetchTodos();
})
const setTodoDone = (id: string, task: string) => {
return (done: boolean) => {
router.todos.updateTodo({ todo: { id, task, done } })
fetchTodos()
}
}
const deleteTodo = (id: string) => {
return () => {
router.todos.deleteTodo({ todo: { id } })
fetchTodos()
}
}
const createTodo = () => {
router.todos.createTodo({ todo: { id: Crypto.randomUUID(), task: todoToCreateTask.value } }).then((r) => {
console.log(r)
fetchTodos()
}).catch((e) => {
console.log(e)
})
}
const updateTodoToCreateTask = (task: string) => {
todoToCreateTask.value = task
}
return (
<Host style={{ flex: 1 }}>
<Column alignment="center" modifiers={[fillMaxWidth()]}>
<Text textStyle={{fontSize:30}}> Create Todo </Text>
<TextInput value={todoToCreateTask} onChangeText={updateTodoToCreateTask} />
<Button modifiers={[controlSize('regular')]} label="create Todo" onPress={createTodo} />
<ScrollView modifiers={[fillMaxWidth()]}>
{
todos.map((todo) => (
<Row alignment="center" modifiers={[fillMaxWidth(),padding(20,0,20,0)]}>
<Column alignment="start" modifiers={[width(200)]}>
<Text> {todo.task} </Text>
</Column>
<Column alignment="center">
<Checkbox value={todo.done || false} onValueChange={setTodoDone(todo.id || "", todo.task)} />
</Column>
<Column alignment="end" modifiers={[fillMaxWidth()]}>
<Button modifiers={[controlSize('regular')]} label="delete" onPress={deleteTodo(todo.id || "")}/>
</Column>
</Row>
))
}
</ScrollView>
</Column>
</Host>
)
}

View File

@@ -0,0 +1,20 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"paths": {
"@/*": [
"./src/*"
],
"@/assets/*": [
"./assets/*"
]
}
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
]
}

View File

@@ -14,7 +14,32 @@
pkgs.protoc-gen-es pkgs.protoc-gen-es
pkgs.cobra-cli pkgs.cobra-cli
]; ];
<@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
reactNative.enable = true;
android-studio = {
enable = false;
};
ndk = {
enable = true;
version = [ "27.1.12297006" ];
};
};
enterShell = ''
export LD_LIBRARY_PATH=$ANDROID_HOME/emulator/lib64:$LD_LIBRARY_PATH
'';
scripts.create-avd.exec = ''
if [ ! -f .avd-created ]; then
avdmanager create avd -n "Pixel_5_API34" -k "system-images;android-34;google_apis_playstore;x86_64" -d "pixel_5";
touch .avd-created;
fi
'';
<@endif>
languages.go.enable = true; languages.go.enable = true;
languages.typescript.enable = true; languages.typescript.enable = true;
services.postgres = { services.postgres = {
@@ -29,26 +54,6 @@
]; ];
}; };
processes = { processes = {
air = {
exec = "air";
cwd = "./services/api";
after = ["devenv:processes:postgres"];
};
protowatcher = {
exec = "watchexec -r -e proto buf generate";
cwd = "./packages/proto";
after= ["devenv:processes:air@started"];
};
protojswatcher = {
exec = "watchexec -e js,ts -w ./packages/rpc/src -r bun run ./scripts/gen-rpc-index.ts";
cwd = "./";
after= ["devenv:processes:protowatcher@started"];
};
sqlwatcher = {
exec = "watchexec -w ./db/migrations -w ./db/query -r -e sql sqlc generate";
cwd = "./services/api";
after= ["devenv:processes:air@started"];
};
<@if(neq(context.project.frontend,"none"))> <@if(neq(context.project.frontend,"none"))>
bundev = { bundev = {
exec = "bun dev"; exec = "bun dev";
@@ -56,5 +61,54 @@
after= ["devenv:processes:air@started"]; after= ["devenv:processes:air@started"];
}; };
<@endif> <@endif>
air = {
exec = "air";
cwd = "./services/api";
after = ["devenv:processes:postgres"];
};
protowatcher = {
exec = "buf generate";
cwd = "./packages/proto";
watch = {
paths = [ ./packages/proto ];
extensions = [ "proto" ];
};
};
protojswatcher = {
exec = "bun run ./scripts/gen-rpc-index.ts";
cwd = "./";
watch = {
paths = [ ./packages/rpc/src ];
extensions = ["js" "ts"];
};
after= ["devenv:processes:protowatcher@completed"];
};
<@if(eq(context.project.mobile,"expo"))>
createavd = {
exec = "create-avd";
};
emulator = {
exec = "emulator -avd Pixel_5_API34";
after = ["devenv:processes:createavd@completed"];
};
expodev = {
exec = "bunx expo run:android";
cwd = "./apps/mobile";
after= ["devenv:processes:emulator@started"];
restart = {
on = "always";
};
};
<@endif>
sqlwatcher = {
exec = "sqlc generate";
watch = {
paths = [ ./db/migrations ./db/query ];
extensions = ["sql" "sqlc"];
};
cwd = "./services/api";
after= ["devenv:processes:air@started"];
};
}; };
} }

View File

@@ -1,4 +1,7 @@
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json <@if(eq(context.project.mobile,"expo"))>
nixpkgs:
allowUnfree: true
<@endif>
inputs: inputs:
nixpkgs: nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling url: github:cachix/devenv-nixpkgs/rolling

View File

@@ -1,16 +1,19 @@
package todo package todo
import ( import (
"cmp"
"context"
"net/http"
"slices"
"time"
"connectrpc.com/connect" "connectrpc.com/connect"
"connectrpc.com/validate" "connectrpc.com/validate"
"context" "github.com/<@var(context.project.name)>/<@var(context.project.name)>/db"
"<@var(context.project.goprefix)>/<@var(context.project.name)>/db"
todov1 "<@var(context.project.goprefix)>/<@var(context.project.name)>/gen/todo/v1" todov1 "<@var(context.project.goprefix)>/<@var(context.project.name)>/gen/todo/v1"
"<@var(context.project.goprefix)>/<@var(context.project.name)>/gen/todo/v1/todov1connect" "<@var(context.project.goprefix)>/<@var(context.project.name)>/gen/todo/v1/todov1connect"
. "<@var(context.project.goprefix)>/<@var(context.project.name)>/utils" . "<@var(context.project.goprefix)>/glstack-test/utils"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
"net/http"
"time"
) )
type TodoServer struct{} type TodoServer struct{}
@@ -46,9 +49,9 @@ func (srv *TodoServer) ListTodos(ctx context.Context, req *connect.Request[todov
if err != nil { if err != nil {
return nil, err return nil, err
} }
reponseTodos := []*todov1.Todo{} responseTodos := []*todov1.Todo{}
for _, todo := range todos { for _, todo := range todos {
reponseTodos = append(reponseTodos, &todov1.Todo{ responseTodos = append(responseTodos, &todov1.Todo{
Id: StrPtr(todo.ID.String()), Id: StrPtr(todo.ID.String()),
Task: todo.Task, Task: todo.Task,
CreatedAt: StrPtr(todo.CreatedAt.Time.Format(time.RFC3339)), CreatedAt: StrPtr(todo.CreatedAt.Time.Format(time.RFC3339)),
@@ -56,10 +59,25 @@ func (srv *TodoServer) ListTodos(ctx context.Context, req *connect.Request[todov
Done: BoolPtr(todo.Done.Bool), Done: BoolPtr(todo.Done.Bool),
}) })
} }
slices.SortFunc(responseTodos, func(a, b *todov1.Todo) int {
dateCmp := cmp.Compare(a.GetCreatedAt(), b.GetCreatedAt())
return dateCmp
})
slices.SortFunc(responseTodos, func(a, b *todov1.Todo) int {
var boolToInt = func(b bool) int {
if b {
return 1
} else {
return 0
}
}
doneCmp := cmp.Compare(boolToInt(*a.Done), boolToInt(*b.Done))
return doneCmp
})
return &connect.Response[todov1.ListTodosResponse]{ return &connect.Response[todov1.ListTodosResponse]{
Msg: &todov1.ListTodosResponse{ Msg: &todov1.ListTodosResponse{
Todos: reponseTodos, Todos: responseTodos,
}, },
}, nil }, nil
} }