Skip to content

Commit dc2fe4d

Browse files
authored
Feat/personalization (#7)
1 parent 3dc5983 commit dc2fe4d

File tree

9 files changed

+314
-28
lines changed

9 files changed

+314
-28
lines changed

packages/client/src/components/app/ChatApp.svelte

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,26 @@
44
import { goto } from "$app/navigation";
55
import Channel from "../channels/Channel.svelte";
66
import ChannelList from "../channels/ChannelList.svelte";
7+
import Personalization from "../chat/Personalization.svelte";
8+
9+
type Selection =
10+
| { type: "chat"; selectedChannelId: Id<"channels"> | undefined }
11+
| { type: "personalization"; selectedChannelId: undefined };
712
813
interface Props {
914
organizationId: Id<"organizations">;
10-
channelId?: Id<"channels">;
15+
screenMode: Selection;
1116
}
1217
13-
const { organizationId, channelId }: Props = $props();
18+
const { organizationId, screenMode }: Props = $props();
1419
1520
const organization = useQuery(api.organizations.get, () => ({
1621
id: organizationId,
1722
}));
1823
</script>
1924

2025
<div class="bg-base-100 flex h-screen">
21-
<div class="bg-base-200 border-base-300 w-80 border-r">
26+
<div class="bg-base-200 border-base-300 flex h-full w-80 flex-col border-r">
2227
<div class="border-base-300 border-b p-4">
2328
<div class="flex items-center justify-between">
2429
<div>
@@ -74,29 +79,42 @@
7479

7580
<ChannelList
7681
{organizationId}
77-
bind:selectedChannelId={
78-
() => channelId,
79-
(id) => {
80-
goto(`/orgs/${organizationId}/chat/${id}`);
82+
bind:screenMode={
83+
() => screenMode,
84+
(screenMode) => {
85+
if (screenMode.type === "chat") {
86+
goto(
87+
`/orgs/${organizationId}/chat/${screenMode.selectedChannelId}`,
88+
);
89+
} else if (screenMode.type === "personalization") {
90+
goto(`/orgs/${organizationId}/personalization`);
91+
}
8192
}
8293
}
8394
/>
8495
</div>
8596

8697
<div class="flex flex-1 flex-col">
87-
{#if channelId}
88-
<Channel {organizationId} 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+
{#if screenMode.type == "chat"}
99+
{#if screenMode.selectedChannelId}
100+
<Channel
101+
{organizationId}
102+
selectedChannelId={screenMode.selectedChannelId}
103+
/>
104+
{:else}
105+
<div class="bg-base-200 flex flex-1 items-center justify-center">
106+
<div class="text-center">
107+
<h2 class="text-base-content/60 mb-2 text-2xl font-semibold">
108+
{organization.data?.name || "組織"}へようこそ
109+
</h2>
110+
<p class="text-base-content/50">
111+
左からチャンネルを選択して会話を始めましょう
112+
</p>
113+
</div>
98114
</div>
99-
</div>
115+
{/if}
116+
{:else if screenMode.type == "personalization"}
117+
<Personalization {organizationId} />
100118
{/if}
101119
</div>
102120
</div>

packages/client/src/components/channels/ChannelList.svelte

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,25 @@
33
import { useQuery } from "convex-svelte";
44
import CreateChannelButton from "./CreateChannelButton.svelte";
55
6+
type Selection =
7+
| { type: "chat"; selectedChannelId: Id<"channels"> | undefined }
8+
| { type: "personalization"; selectedChannelId: undefined };
9+
10+
/*<<<<<<< HEAD:packages/client/src/components/chat/ChannelList.svelte
11+
screenMode: Selection;
12+
}
13+
14+
let {
15+
screenMode = $bindable(),
16+
}: Props = $props();
17+
=======
18+
*/
619
interface Props {
720
organizationId: Id<"organizations">;
8-
selectedChannelId?: Id<"channels">;
21+
screenMode: Selection;
922
}
1023
11-
let { organizationId, selectedChannelId = $bindable() }: Props = $props();
24+
let { organizationId, screenMode = $bindable() }: Props = $props();
1225
1326
const channels = useQuery(api.channels.list, () => ({
1427
organizationId,
@@ -27,11 +40,13 @@
2740
<button
2841
class={[
2942
"border-base-300 w-full border-b p-3 text-left",
30-
selectedChannelId === channel._id
43+
screenMode.selectedChannelId === channel._id
3144
? "bg-primary text-primary-content"
3245
: "hover:bg-base-300",
3346
].join(" ")}
34-
onclick={() => (selectedChannelId = channel._id)}
47+
onclick={() => {
48+
screenMode = { type: "chat", selectedChannelId: channel._id };
49+
}}
3550
>
3651
<div class="font-medium"># {channel.name}</div>
3752
{#if channel.description}
@@ -51,4 +66,10 @@
5166
</div>
5267
{/if}
5368
</div>
69+
<button
70+
class="btn btn-primary mt-auto mb-2 w-full"
71+
onclick={() => {
72+
screenMode = { type: "personalization", selectedChannelId: undefined };
73+
}}>個人用設定</button
74+
>
5475
</div>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<script lang="ts">
2+
import { api, type Id } from "@packages/convex";
3+
import { useConvexClient, useQuery } from "convex-svelte";
4+
5+
const { organizationId }: { organizationId: Id<"organizations"> } = $props();
6+
7+
const convex = useConvexClient();
8+
9+
const identity = useQuery(api.users.me, {});
10+
const personalization = useQuery(api.personalization.getPersonalization, {
11+
organizationId: organizationId,
12+
});
13+
let iconURL = $state<string | null>("");
14+
let imageURL = $derived(iconURL || identity.data?.image);
15+
let userName = $derived(
16+
personalization.data?.nickname || identity.data?.name,
17+
);
18+
let changedImage = $state<string>("");
19+
let changedImageFile = $state<File | undefined>();
20+
let changedUserName = $state<string>("");
21+
22+
$effect(() => {
23+
if (userName) {
24+
changedUserName = userName;
25+
}
26+
if (personalization.data) {
27+
new Promise((resolve) => {
28+
resolve(personalization.data?.icon);
29+
})
30+
.then((value) => {
31+
return new Promise((resolve, reject) => {
32+
const storageId = value as Id<"_storage">;
33+
if (storageId) {
34+
resolve(
35+
convex.mutation(api.personalization.getImageUrl, {
36+
storageId: storageId,
37+
}),
38+
);
39+
} else {
40+
reject();
41+
}
42+
});
43+
})
44+
.then((value) => {
45+
const url = value as string;
46+
iconURL = url;
47+
});
48+
}
49+
});
50+
51+
async function save() {
52+
const image = changedImageFile;
53+
changedImage = "";
54+
changedImageFile = undefined;
55+
if (changedUserName?.trim() && !(userName === changedUserName)) {
56+
await convex.mutation(api.personalization.save, {
57+
nickName: changedUserName,
58+
organizationId: organizationId,
59+
});
60+
}
61+
if (image) {
62+
const postUrl = await convex.mutation(
63+
api.personalization.generateUploadUrl,
64+
{},
65+
);
66+
const result = await fetch(postUrl, {
67+
method: "POST",
68+
headers: { "Content-Type": image.type },
69+
body: image,
70+
});
71+
72+
const { storageId } = await result.json();
73+
74+
await convex.mutation(api.personalization.saveImage, {
75+
icon: storageId,
76+
organizationId: organizationId,
77+
});
78+
}
79+
}
80+
</script>
81+
82+
<h2 class="py-2 text-center text-lg font-semibold">アイコンの変更</h2>
83+
<div class="flex justify-around">
84+
<div class="w-32 flex-col">
85+
<p class="mb-2 text-center">変更前</p>
86+
<img src={imageURL} alt="googleアイコン" class="w-32" />
87+
</div>
88+
{#if changedImage}
89+
<div class="w-32 flex-col">
90+
<p class="mb-2 text-center">変更後</p>
91+
<img src={changedImage} alt="変更後" class="w-32" />
92+
</div>
93+
{/if}
94+
</div>
95+
<input
96+
type="file"
97+
class="file:bg-primary file:text-primary-content
98+
text-sm
99+
text-gray-500 file:mr-4 file:ml-2
100+
file:rounded file:border-0
101+
file:px-4
102+
file:py-2 file:font-semibold"
103+
accept=".jpg, .png"
104+
onchange={(event) => {
105+
const input = event.target as HTMLInputElement;
106+
const file = input.files?.[0];
107+
if (file) {
108+
changedImage = URL.createObjectURL(file);
109+
changedImageFile = file;
110+
}
111+
}}
112+
/>
113+
<h2 class="py-2 text-center text-lg font-semibold">名前の変更</h2>
114+
<div class="flex justify-around">
115+
<div>
116+
<h3 class="text-center text-base">変更前</h3>
117+
<h4 class="text-lg">{userName}</h4>
118+
</div>
119+
<div>
120+
<h3 class="text-center text-base">変更後</h3>
121+
<input
122+
type="text"
123+
placeholder="ユーザー名"
124+
class="input input-primary w-full"
125+
bind:value={changedUserName}
126+
/>
127+
</div>
128+
</div>
129+
130+
<button class="btn btn-primary mt-auto mr-2 mb-2 ml-auto w-16" onclick={save}
131+
>保存</button
132+
>

packages/client/src/components/organization/OrganizationSelector.svelte

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,7 @@
5454
{#if organizations.data && organizations.data.length === 0}
5555
<div class="py-8 text-center">
5656
<p class="text-base-content/60 mb-4">参加している組織がありません</p>
57-
<a href="/organizations/create" class="btn btn-primary">
58-
新しい組織を作成
59-
</a>
57+
<a href="/orgs/new" class="btn btn-primary"> 新しい組織を作成 </a>
6058
</div>
6159
{/if}
6260
</div>

packages/client/src/routes/orgs/[orgId]/+page.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@
66
const orgId = $derived(page.params.orgId as Id<"organizations">);
77
</script>
88

9-
<ChatApp organizationId={orgId} channelId={undefined} />
9+
<ChatApp
10+
organizationId={orgId}
11+
screenMode={{ type: "chat", selectedChannelId: undefined }}
12+
/>

packages/client/src/routes/orgs/[orgId]/chat/[channelId]/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88

99
<ChatApp
1010
organizationId={orgId as Id<"organizations">}
11-
channelId={channelId as Id<"channels">}
11+
screenMode={{ type: "chat", selectedChannelId: channelId as Id<"channels"> }}
1212
/>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script lang="ts">
2+
import { type Id } from "@packages/convex";
3+
import { page } from "$app/state";
4+
import ChatApp from "~/components/app/ChatApp.svelte";
5+
6+
const { orgId } = $derived(page.params);
7+
</script>
8+
9+
<ChatApp
10+
organizationId={orgId as Id<"organizations">}
11+
screenMode={{ type: "personalization", selectedChannelId: undefined }}
12+
/>

0 commit comments

Comments
 (0)