Skip to content

Commit b22bbc7

Browse files
aster-voidclaude
andauthored
Feat: Organization (#4)
Co-authored-by: Claude <[email protected]>
1 parent 22b1810 commit b22bbc7

34 files changed

+1268
-88
lines changed

CLAUDE.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ This is a TypeScript monorepo using a Convex backend and SvelteKit frontend with
99
### Stack
1010

1111
- **Frontend**: SvelteKit with Svelte 5, TypeScript, TailwindCSS, DaisyUI
12+
- **CRITICAL**: This project uses Svelte 5 RUNES MODE - NEVER use legacy reactive statements (`$:`)
13+
- **ALWAYS use**: `$state`, `$derived`, `$effect` instead of legacy syntax
1214
- **Backend**: Convex (real-time database and functions)
1315
- **Desktop**: Tauri (optional, conflicts with web dev server)
1416
- **Internationalization**: Paraglide for i18n (English/Japanese)
@@ -124,6 +126,22 @@ When working with Svelte code, always reference the latest documentation:
124126
</script>
125127
```
126128

129+
### Mutations with useMutation
130+
131+
Since `convex-svelte` doesn't export `useMutation`, we have a custom utility at `src/lib/useMutation.svelte.ts`:
132+
133+
```typescript
134+
import { useMutation } from "~/lib/useMutation.svelte.ts";
135+
136+
const createOrganization = useMutation(api.organizations.create);
137+
138+
// Use like this
139+
await createOrganization.run({ name: "New Org", description: "..." });
140+
// which exposes these properties
141+
createOrganization.processing; // boolean, use for button disabled state / loading spinners
142+
createOrganization.error; // string | null, use for error messages
143+
```
144+
127145
### Backend (Convex)
128146

129147
- **Schema**: Defined in `packages/convex/src/convex/schema.ts`
@@ -169,3 +187,15 @@ bun dev:tauri
169187
```
170188

171189
Tauri conflicts with the web development server and requires more resources for compilation.
190+
191+
## Coding Instructions
192+
193+
- **🚫 NEVER USE LEGACY SVELTE SYNTAX**: This project uses Svelte 5 runes mode
194+
- ❌ FORBIDDEN: `$: reactiveVar = ...` (reactive statements)
195+
- ❌ FORBIDDEN: `let count = 0` for reactive state
196+
- ✅ REQUIRED: `const reactiveVar = $derived(...)`
197+
- ✅ REQUIRED: `let count = $state(0)` for reactive state
198+
- ✅ REQUIRED: `$effect(() => { ... })` for side effects
199+
- Always prefer using DaisyUI classes, and use minimal Tailwind classes.
200+
- Separate components into smallest pieces for readability.
201+
- Name snippets with camelCase instead of PascalCase to avoid confusion with components.

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,16 @@ git commit -m "It's not working??" -n
8282
const selectedChannel = useQuery(api.channels.get, { id: selectedChannelId });
8383
</script>
8484
```
85+
86+
### (client) Icon の使用について
87+
88+
- unplugin-icons を使っています。 <https://github.com/unplugin/unplugin-icons>
89+
- Usage Example: `import MdiClose from "~icons/mdi/close"`
90+
- 現在インストールされているアイコンセットは以下のとおりです:
91+
- mdi (Material Design Icons)
92+
- 新規アイコンセットを追加する場合は、`cd packages/client; bun add @iconify-json/[iconset]` で追加できます。
93+
- icon の一覧はここで見れます。: https://icones.js.org/
94+
95+
### 独自命名規則
96+
97+
- Snippet の命名は camelCase で行います。 (PascalCase はコンポーネントと混同されるため)

biome.jsonc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
"noUnusedTemplateLiteral": "error",
2727
"useNumberNamespace": "error",
2828
"noInferrableTypes": "error",
29-
"noUselessElse": "error",
3029
},
3130
"correctness": {
3231
"useImportExtensions": {

bun.lock

Lines changed: 43 additions & 0 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
{
22
"name": "prism",
3-
"type": "module",
3+
"devDependencies": {
4+
"@biomejs/biome": "^2.1.2",
5+
"lefthook": "^1.12.2",
6+
"prettier": "^3.6.2",
7+
"prettier-plugin-svelte": "^3.4.0",
8+
"prettier-plugin-tailwindcss": "^0.6.14"
9+
},
10+
"peerDependencies": {
11+
"typescript": "^5.8.3"
12+
},
413
"private": true,
5-
"workspaces": [
6-
"packages/*"
7-
],
814
"scripts": {
915
"dev": "bun run --filter='@packages/{client,convex}' dev",
1016
"dev:all": "(trap 'kill 0' EXIT; bun run dev:convex & bun run dev:web & bun run dev:storybook & wait",
@@ -24,14 +30,8 @@
2430
"convex": "cd packages/convex && bun run convex",
2531
"paraglide": "cd packages/client && bun run paraglide"
2632
},
27-
"peerDependencies": {
28-
"typescript": "^5.8.3"
29-
},
30-
"devDependencies": {
31-
"@biomejs/biome": "^2.1.2",
32-
"lefthook": "^1.12.2",
33-
"prettier": "^3.6.2",
34-
"prettier-plugin-svelte": "^3.4.0",
35-
"prettier-plugin-tailwindcss": "^0.6.14"
36-
}
33+
"type": "module",
34+
"workspaces": [
35+
"packages/*"
36+
]
3737
}

packages/client/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,18 @@
4141
"dependencies": {
4242
"@auth/core": "^0.40.0",
4343
"@convex-dev/auth": "^0.0.87",
44+
"@iconify-json/mdi": "^1.2.3",
45+
"@inlang/paraglide-js": "^2.2.0",
4446
"@mmailaender/convex-auth-svelte": "^0.0.2",
4547
"@packages/convex": "workspace:*",
46-
"@inlang/paraglide-js": "^2.2.0",
4748
"@playwright/test": "^1.54.1",
4849
"@sveltejs/adapter-static": "^3.0.8",
4950
"@tauri-apps/api": "^2.6.0",
5051
"@tauri-apps/plugin-opener": "^2.4.0",
5152
"convex": "^1.25.4",
5253
"convex-svelte": "^0.0.11",
53-
"runed": "^0.31.0"
54+
"robot3": "^1.1.1",
55+
"runed": "^0.31.0",
56+
"unplugin-icons": "^22.2.0"
5457
}
5558
}

packages/client/src/app.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/// <reference types="@sveltejs/kit" />
2+
/// <reference types="unplugin-icons/types/svelte" />
3+
14
// See https://svelte.dev/docs/kit/types#app.d.ts
25
// for information about these interfaces
36
declare global {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<script lang="ts">
2+
import { api, type Id } from "@packages/convex";
3+
import { useQuery } from "convex-svelte";
4+
import { goto } from "$app/navigation";
5+
import Channel from "../channels/Channel.svelte";
6+
import ChannelList from "../channels/ChannelList.svelte";
7+
8+
interface Props {
9+
organizationId: Id<"organizations">;
10+
channelId?: Id<"channels">;
11+
}
12+
13+
const { organizationId, channelId }: Props = $props();
14+
15+
const organization = useQuery(api.organizations.get, () => ({
16+
id: organizationId,
17+
}));
18+
</script>
19+
20+
<div class="bg-base-100 flex h-screen">
21+
<div class="bg-base-200 border-base-300 w-80 border-r">
22+
<div class="border-base-300 border-b p-4">
23+
<div class="flex items-center justify-between">
24+
<div>
25+
<h2 class="text-base-content text-lg font-bold">
26+
{organization.data?.name || "組織"}
27+
</h2>
28+
{#if organization.data?.description}
29+
<p class="text-base-content/70 text-sm">
30+
{organization.data.description}
31+
</p>
32+
{/if}
33+
</div>
34+
<div class="dropdown dropdown-end">
35+
<div
36+
tabindex="0"
37+
role="button"
38+
class="btn btn-ghost btn-sm btn-circle"
39+
>
40+
<svg
41+
xmlns="http://www.w3.org/2000/svg"
42+
fill="none"
43+
viewBox="0 0 24 24"
44+
class="inline-block h-4 w-4 stroke-current"
45+
>
46+
<path
47+
stroke-linecap="round"
48+
stroke-linejoin="round"
49+
stroke-width="2"
50+
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4"
51+
></path>
52+
</svg>
53+
</div>
54+
<ul
55+
role="menu"
56+
tabindex="0"
57+
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
58+
>
59+
<li role="menuitem">
60+
<a href="/orgs/{organizationId}/settings">組織設定</a>
61+
</li>
62+
<li role="menuitem">
63+
<a href="/">組織選択</a>
64+
</li>
65+
</ul>
66+
</div>
67+
</div>
68+
{#if organization.data?.permission}
69+
<div class="badge badge-outline mt-2 capitalize">
70+
{organization.data.permission}
71+
</div>
72+
{/if}
73+
</div>
74+
75+
<ChannelList
76+
{organizationId}
77+
bind:selectedChannelId={
78+
() => channelId,
79+
(id) => {
80+
goto(`/orgs/${organizationId}/chat/${id}`);
81+
}
82+
}
83+
/>
84+
</div>
85+
86+
<div class="flex flex-1 flex-col">
87+
{#if channelId}
88+
<Channel selectedChannelId={channelId} />
89+
{:else}
90+
<div class="bg-base-200 flex flex-1 items-center justify-center">
91+
<div class="text-center">
92+
<h2 class="text-base-content/60 mb-2 text-2xl font-semibold">
93+
{organization.data?.name || "組織"}へようこそ
94+
</h2>
95+
<p class="text-base-content/50">
96+
左からチャンネルを選択して会話を始めましょう
97+
</p>
98+
</div>
99+
</div>
100+
{/if}
101+
</div>
102+
</div>

packages/client/src/components/chat/Channel.svelte renamed to packages/client/src/components/channels/Channel.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import { api, type Id } from "@packages/convex";
33
import type { Doc } from "@packages/convex/src/convex/_generated/dataModel";
44
import { useQuery } from "convex-svelte";
5-
import MessageInput from "./MessageInput.svelte";
6-
import MessageList from "./MessageList.svelte";
5+
import MessageInput from "../chat/MessageInput.svelte";
6+
import MessageList from "../chat/MessageList.svelte";
77
88
interface Props {
99
selectedChannelId: Id<"channels">;
@@ -12,7 +12,7 @@
1212
let { selectedChannelId }: Props = $props();
1313
1414
const selectedChannel = useQuery(api.channels.get, () => ({
15-
id: selectedChannelId,
15+
channelId: selectedChannelId,
1616
}));
1717
1818
let replyingTo = $state<Doc<"messages"> | null>(null);

packages/client/src/components/chat/ChannelList.svelte renamed to packages/client/src/components/channels/ChannelList.svelte

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,24 @@
11
<script lang="ts">
22
import { api, type Id } from "@packages/convex";
3-
import { useConvexClient, useQuery } from "convex-svelte";
3+
import { useQuery } from "convex-svelte";
4+
import CreateChannelButton from "./CreateChannelButton.svelte";
45
56
interface Props {
7+
organizationId: Id<"organizations">;
68
selectedChannelId?: Id<"channels">;
79
}
810
9-
let { selectedChannelId = $bindable(undefined) }: Props = $props();
11+
let { organizationId, selectedChannelId = $bindable() }: Props = $props();
1012
11-
const convex = useConvexClient();
12-
const channels = useQuery(api.channels.list, () => ({}));
13-
14-
async function createChannel() {
15-
const name = prompt("チャンネル名を入力してください:");
16-
if (name?.trim()) {
17-
await convex.mutation(api.channels.create, { name: name.trim() });
18-
}
19-
}
13+
const channels = useQuery(api.channels.list, () => ({
14+
organizationId,
15+
}));
2016
</script>
2117

22-
<div class="bg-base-200 flex h-full w-64 flex-col">
18+
<div class="flex h-full flex-col">
2319
<div class="border-base-300 border-b p-4">
24-
<h2 class="text-lg font-semibold">チャンネル</h2>
25-
<button class="btn btn-primary btn-sm mt-2 w-full" onclick={createChannel}>
26-
+ 新しいチャンネル
27-
</button>
20+
<h3 class="text-base font-semibold">チャンネル</h3>
21+
<CreateChannelButton {organizationId} />
2822
</div>
2923

3024
<div class="flex-1 overflow-y-auto">
@@ -50,5 +44,11 @@
5044
チャンネルを読み込み中...
5145
</div>
5246
{/if}
47+
48+
{#if channels.data && channels.data.length === 0}
49+
<div class="text-base-content/60 p-4 text-center text-sm">
50+
まだチャンネルがありません
51+
</div>
52+
{/if}
5353
</div>
5454
</div>

0 commit comments

Comments
 (0)