From cbf18aec8f17ebad87b5431137d4dec230b27015 Mon Sep 17 00:00:00 2001 From: Last2014 Date: Sun, 31 May 2026 13:54:55 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=81=AE=E9=80=81=E5=8F=97=E4=BF=A1=20/=20New:=20?= =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=AEicon=E3=83=97?= =?UTF-8?q?=E3=83=AD=E3=83=91=E3=83=86=E3=82=A3=20/=20New:=20log=E3=82=A8?= =?UTF-8?q?=E3=83=B3=E3=83=86=E3=82=A3=E3=83=86=E3=82=A3=E3=83=BB=E3=83=AA?= =?UTF-8?q?=E3=83=9D=E3=82=B8=E3=83=88=E3=83=AA=20/=20Chg:=20=E3=82=B3?= =?UTF-8?q?=E3=83=9F=E3=83=A5=E3=83=8B=E3=83=86=E3=82=A3=E3=83=AA=E3=83=9D?= =?UTF-8?q?=E3=82=B8=E3=83=88=E3=83=AA=E3=81=AE=E3=82=B9=E3=82=AD=E3=83=BC?= =?UTF-8?q?=E3=83=9E=E3=81=AEicon=E3=82=92optional=E3=81=AB=20/=20Del:=20?= =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AAimport=20/=20Fix:=20Vue=E8=B5=B7?= =?UTF-8?q?=E5=8B=95=E5=89=8D=E3=81=AEindex.html=E3=81=AE=E8=83=8C?= =?UTF-8?q?=E6=99=AF=E8=89=B2=E3=82=92Vue=E3=81=A8=E5=90=8C=E6=9C=9F=20/?= =?UTF-8?q?=20Enhance:=20Service=20Worker=E3=82=92=E6=94=B9=E5=96=84=20/?= =?UTF-8?q?=20Fix:=20=E6=9C=80=E5=88=9D=E3=81=AB=E9=96=8B=E3=81=84?= =?UTF-8?q?=E3=81=9F=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=8C=E5=8B=95=E4=BD=9C?= =?UTF-8?q?=E3=81=97=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=20/=20Feat:=20?= =?UTF-8?q?=E4=B8=8A=E9=83=A8=E3=81=AE=E9=80=9A=E7=9F=A5=E3=83=A2=E3=83=BC?= =?UTF-8?q?=E3=83=80=E3=83=AB=20/=20Feat:=20=E9=96=89=E3=81=98=E3=82=8B?= =?UTF-8?q?=E3=81=93=E3=81=A8=E3=81=8C=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AE=E3=83=A2=E3=83=BC=E3=83=80?= =?UTF-8?q?=E3=83=AB=E3=81=AB=E5=86=8D=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=81=BF?= =?UTF-8?q?=E3=83=9C=E3=82=BF=E3=83=B3=E3=82=92=E8=BF=BD=E5=8A=A0=20/=20Fi?= =?UTF-8?q?x:=20=E3=81=AF=E3=81=BF=E5=87=BA=E3=81=99=E6=8C=99=E5=8B=95?= =?UTF-8?q?=E3=81=AA=E3=81=A9=E3=81=AECSS=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/package.json | 2 +- packages/backend/src/lib/object.ts | 4 + packages/backend/src/modules/entities/Log.ts | 37 +++ .../backend/src/modules/entities/Message.ts | 5 +- packages/backend/src/modules/entities/User.ts | 6 + .../src/modules/repositories/Community.ts | 2 +- .../backend/src/modules/repositories/Log.ts | 17 + .../src/modules/repositories/Message.ts | 63 ++++ .../backend/src/modules/repositories/User.ts | 1 + packages/backend/src/routes/community/list.ts | 1 - packages/backend/src/routes/index.ts | 5 + packages/backend/src/routes/message/delete.ts | 37 +++ packages/backend/src/routes/message/index.ts | 18 ++ packages/backend/src/routes/message/list.ts | 55 ++++ packages/backend/src/routes/message/send.ts | 39 +++ packages/frontend/index.html | 4 +- packages/frontend/src/Layout.vue | 45 ++- packages/frontend/src/components/Message.vue | 187 +++++++++++ .../frontend/src/components/Modal/Confirm.vue | 60 ++++ .../frontend/src/components/Modal/Error.vue | 9 + .../src/components/Modal/TopNotice.vue | 75 +++++ packages/frontend/src/global.css | 3 +- packages/frontend/src/lib/modal.ts | 65 ++-- packages/frontend/src/lib/parser.ts | 28 ++ packages/frontend/src/lib/sw.ts | 56 +++- packages/frontend/src/main.ts | 2 +- .../frontend/src/routes/community/channel.vue | 303 +++++++++++++++++- .../frontend/src/routes/community/index.vue | 20 +- packages/frontend/vite.config.ts | 27 +- .../src/1.0.0-alpha.0/api/message/delete.d.ts | 17 + .../src/1.0.0-alpha.0/api/message/list.d.ts | 21 ++ .../src/1.0.0-alpha.0/api/message/send.d.ts | 20 ++ .../lynqchat-js/src/1.0.0-alpha.0/map.d.ts | 8 +- .../src/1.0.0-alpha.0/modules/channel.d.ts | 2 +- .../src/1.0.0-alpha.0/modules/community.d.ts | 2 +- .../src/1.0.0-alpha.0/modules/message.d.ts | 9 + .../src/1.0.0-alpha.0/modules/user.d.ts | 2 + 37 files changed, 1174 insertions(+), 83 deletions(-) create mode 100644 packages/backend/src/lib/object.ts create mode 100644 packages/backend/src/modules/entities/Log.ts create mode 100644 packages/backend/src/modules/repositories/Log.ts create mode 100644 packages/backend/src/modules/repositories/Message.ts create mode 100644 packages/backend/src/routes/message/delete.ts create mode 100644 packages/backend/src/routes/message/index.ts create mode 100644 packages/backend/src/routes/message/list.ts create mode 100644 packages/backend/src/routes/message/send.ts create mode 100644 packages/frontend/src/components/Message.vue create mode 100644 packages/frontend/src/components/Modal/Confirm.vue create mode 100644 packages/frontend/src/components/Modal/TopNotice.vue create mode 100644 packages/frontend/src/lib/parser.ts create mode 100644 packages/lynqchat-js/src/1.0.0-alpha.0/api/message/delete.d.ts create mode 100644 packages/lynqchat-js/src/1.0.0-alpha.0/api/message/list.d.ts create mode 100644 packages/lynqchat-js/src/1.0.0-alpha.0/api/message/send.d.ts create mode 100644 packages/lynqchat-js/src/1.0.0-alpha.0/modules/message.d.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index ec37ff9..5065681 100755 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -22,12 +22,12 @@ "@mikro-orm/postgresql": "^6.6.7", "@mikro-orm/reflection": "^6.6.7", "@types/web-push": "^3.6.4", - "lynqchat-js": "workspace:*", "argon2": "^0.44.0", "cross-env": "^10.1.0", "fastify": "^5.7.4", "fastify-plugin": "^5.1.0", "fs": "0.0.1-security", + "lynqchat-js": "workspace:*", "os": "^0.1.2", "tsc-alias": "^1.8.16", "typescript": "^5.9.3", diff --git a/packages/backend/src/lib/object.ts b/packages/backend/src/lib/object.ts new file mode 100644 index 0000000..911c23d --- /dev/null +++ b/packages/backend/src/lib/object.ts @@ -0,0 +1,4 @@ +export const Pick = (object: T, keys: (keyof T)[]) => + Object.entries(object) + .filter(([key]) => keys.includes(key as keyof T)) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); \ No newline at end of file diff --git a/packages/backend/src/modules/entities/Log.ts b/packages/backend/src/modules/entities/Log.ts new file mode 100644 index 0000000..02ded5a --- /dev/null +++ b/packages/backend/src/modules/entities/Log.ts @@ -0,0 +1,37 @@ +import { Entity, EntityRepositoryType, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core"; +import { LogRepository } from "@/modules/repositories/Log"; +import generateUniqueId from "@/lib/id"; +import { UserEntity } from "@/modules/entities/User"; + +@Entity({ + tableName: "log", + repository: () => LogRepository, +}) +export class LogEntity { + [OptionalProps]?: "id" | "createdBy" | "createdAt"; + [EntityRepositoryType]?: LogRepository; + + @PrimaryKey({ + type: "string", + length: 10, + onCreate: () => generateUniqueId(), + }) + id!: string; + + @Property({ + type: "string", + length: 4096 + }) + text!: string; + + @ManyToOne(() => UserEntity, { + nullable: true, + }) + createdBy?: UserEntity; + + @Property({ + type: "datetime", + onCreate: () => new Date(), + }) + createdAt!: Date; +} \ No newline at end of file diff --git a/packages/backend/src/modules/entities/Message.ts b/packages/backend/src/modules/entities/Message.ts index ff4900c..38afd6d 100644 --- a/packages/backend/src/modules/entities/Message.ts +++ b/packages/backend/src/modules/entities/Message.ts @@ -1,12 +1,15 @@ import generateUniqueId from "@/lib/id"; -import { Entity, Index, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core"; +import { Entity, EntityRepositoryType, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core"; import { UserEntity } from "@/modules/entities/User"; import { ChannelEntity } from "@/modules/entities/Channel"; +import { MessageRepository } from "@/modules/repositories/Message"; @Entity({ tableName: "message", + repository: () => MessageRepository, }) export class MessageEntity { + [EntityRepositoryType]?: MessageRepository; [OptionalProps]?: "id" | "createdAt"; @PrimaryKey({ diff --git a/packages/backend/src/modules/entities/User.ts b/packages/backend/src/modules/entities/User.ts index ff1b0b1..09ed5d2 100644 --- a/packages/backend/src/modules/entities/User.ts +++ b/packages/backend/src/modules/entities/User.ts @@ -30,6 +30,12 @@ export class UserEntity { }) username!: string; + @Property({ + type: "string", + nullable: true, + }) + icon?: string; + @Property({ type: "string", length: 4096, diff --git a/packages/backend/src/modules/repositories/Community.ts b/packages/backend/src/modules/repositories/Community.ts index 6ff32fc..00590be 100644 --- a/packages/backend/src/modules/repositories/Community.ts +++ b/packages/backend/src/modules/repositories/Community.ts @@ -9,7 +9,7 @@ export class CommunityRepository extends EntityRepository { name: z.string().trim().min(1).max(20), description: z.string().trim().min(1).max(4096), userid: UserRepository.schema.shape.userid, - icon: z.string().url(), + icon: z.string().url().optional(), }); async listCommunity(limit: number = 20, sinceData?: string) { diff --git a/packages/backend/src/modules/repositories/Log.ts b/packages/backend/src/modules/repositories/Log.ts new file mode 100644 index 0000000..4940211 --- /dev/null +++ b/packages/backend/src/modules/repositories/Log.ts @@ -0,0 +1,17 @@ +import { EntityRepository } from "@mikro-orm/postgresql"; +import type { LogEntity } from "@/modules/entities/Log"; +import z from "zod/v3"; + +export class LogRepository extends EntityRepository { + public static schema = z.string().min(1).max(4096); + + async insertLog(text: string, userid?: string) { + const log = this.create({ + text, + createdBy: userid, + }); + + await this.em.persist(log).flush(); + return log.id; + } +} diff --git a/packages/backend/src/modules/repositories/Message.ts b/packages/backend/src/modules/repositories/Message.ts new file mode 100644 index 0000000..320830a --- /dev/null +++ b/packages/backend/src/modules/repositories/Message.ts @@ -0,0 +1,63 @@ +import { EntityRepository } from "@mikro-orm/postgresql"; +import type { MessageEntity } from "@/modules/entities/Message"; +import z from "zod/v3"; +import { ErrorBase } from "@/errors"; + +export class MessageRepository extends EntityRepository { + public static schema = z.object({ + message: z.string().trim().min(0).max(4096), + channel: z.string().length(10), + userid: z.string().length(10), + }); + + async sendMessage(messageText: string, channel: string, user: string) { + const message = this.create({ + message: messageText, + channel, + createdBy: user, + }); + + await this.em.persist(message).flush(); + return message.id; + } + + async deleteMessage(id: string) { + const message = this.getReference(id); + await this.em.remove(message).flush(); + } + + async listMessage(channel: string, limit: number = 20, untilData?: string) { + let until = untilData ?? new Date(); + + if ( + untilData && + isNaN(new Date(untilData).getTime()) + ) { + const itMessage = await this.findOne({ id: untilData }); + + if (!itMessage) { + return ErrorBase({ + bad: "client", + code: "message_not_found", + message: "対象のメッセージが見つかりませんでした。", + }); + } + + until = itMessage.createdAt; + } + + const findResult = await this.find({ + channel, + createdAt: { + $lt: until, + }, + }, { + orderBy: { + createdAt: "DESC", + }, + limit: limit, + }); + + return findResult ?? []; + } +} diff --git a/packages/backend/src/modules/repositories/User.ts b/packages/backend/src/modules/repositories/User.ts index e1636e3..6ec8812 100644 --- a/packages/backend/src/modules/repositories/User.ts +++ b/packages/backend/src/modules/repositories/User.ts @@ -8,6 +8,7 @@ export class UserRepository extends EntityRepository { public static schema = z.object({ userid: z.string().trim().min(3).max(20), username: z.string().trim().min(3).max(30), + icon: z.string().url().optional(), profile: z.string().max(4096).optional(), email: z.string().trim().min(6).max(254).regex(EmailRegex), password: z.string().trim().min(8), diff --git a/packages/backend/src/routes/community/list.ts b/packages/backend/src/routes/community/list.ts index ea15540..f51f9b4 100644 --- a/packages/backend/src/routes/community/list.ts +++ b/packages/backend/src/routes/community/list.ts @@ -1,7 +1,6 @@ import { DatabaseError, InputError } from "@/errors"; import Logger from "@/lib/logger"; import { CommunityEntity } from "@/modules/entities/Community"; -import { UserEntity } from "@/modules/entities/User"; import type { FastifyInstance } from "fastify"; import z from "zod/v3"; diff --git a/packages/backend/src/routes/index.ts b/packages/backend/src/routes/index.ts index 9381e90..3da48b3 100755 --- a/packages/backend/src/routes/index.ts +++ b/packages/backend/src/routes/index.ts @@ -5,6 +5,7 @@ import Me from "./me"; import ServerInfo from "./server-info"; import Community from "./community"; import Channel from "./channel"; +import Message from "./message"; export default async function Routes(fastify: FastifyInstance) { await fastify.register(Setup, { @@ -30,4 +31,8 @@ export default async function Routes(fastify: FastifyInstance) { await fastify.register(Channel, { prefix: "/channel", }); + + await fastify.register(Message, { + prefix: "/message", + }); } diff --git a/packages/backend/src/routes/message/delete.ts b/packages/backend/src/routes/message/delete.ts new file mode 100644 index 0000000..d70e2e4 --- /dev/null +++ b/packages/backend/src/routes/message/delete.ts @@ -0,0 +1,37 @@ +import { DatabaseError, InputError } from "@/errors"; +import Logger from "@/lib/logger"; +import { MessageEntity } from "@/modules/entities/Message"; +import type { FastifyInstance } from "fastify"; +import z from "zod/v3"; + +export default async function MessageDelete(fastify: FastifyInstance) { + const logger = new Logger("Endpoint | message/delete"); + + fastify.post("/", async (req, res) => { + res.header("Content-Type", "application/json"); + + if ("error" in req.token) + return res.code(400).send(req.token); + + const result = z.object({ + id: z.string().length(10), + }).safeParse(req.body); + + if (!result.success) { + return res.code(400).send(InputError(result.error.issues)); + } + + try { + const messageRepo = fastify.orm.em.getRepository(MessageEntity); + await messageRepo.deleteMessage(result.data.id); + + return res.send({ + success: true, + }); + } catch (err) { + logger.error("Database Error:", err); + + return res.code(500).send(DatabaseError()); + } + }); +} \ No newline at end of file diff --git a/packages/backend/src/routes/message/index.ts b/packages/backend/src/routes/message/index.ts new file mode 100644 index 0000000..eb6b7ea --- /dev/null +++ b/packages/backend/src/routes/message/index.ts @@ -0,0 +1,18 @@ +import type { FastifyInstance } from "fastify"; +import MessageList from "./list"; +import MessageSend from "./send"; +import MessageDelete from "./delete"; + +export default async function Message(fastify: FastifyInstance) { + await fastify.register(MessageList, { + prefix: "/list", + }); + + await fastify.register(MessageSend, { + prefix: "/send", + }); + + await fastify.register(MessageDelete, { + prefix: "/delete", + }); +} \ No newline at end of file diff --git a/packages/backend/src/routes/message/list.ts b/packages/backend/src/routes/message/list.ts new file mode 100644 index 0000000..bff0b07 --- /dev/null +++ b/packages/backend/src/routes/message/list.ts @@ -0,0 +1,55 @@ +import { DatabaseError, InputError } from "@/errors"; +import Logger from "@/lib/logger"; +import { Pick } from "@/lib/object"; +import { MessageEntity } from "@/modules/entities/Message"; +import type { FastifyInstance } from "fastify"; +import z from "zod/v3"; + +export default async function MessageList(fastify: FastifyInstance) { + const logger = new Logger("Endpoint | message/list"); + + fastify.post("/", async (req, res) => { + res.header("Content-Type", "application/json"); + + if ("error" in req.token) + return res.code(400).send(req.token); + + const bodySchema = z.object({ + limit: z.number().optional(), + until: z.string().optional(), + channel: z.string().length(10), + }); + + const result = bodySchema.safeParse(req.body); + + if (!result.success) { + return res.code(400).send(InputError(result.error.issues)); + } + + try { + const messageRepo = fastify.orm.em.getRepository(MessageEntity); + + const findResult = await messageRepo.listMessage( + result.data.channel, + result.data.limit, + result.data.until, + ); + + if ("error" in findResult) { + return res.code(400).send(findResult); + } + + return res.send({ + success: true, + messages: findResult.map(message => ({ + ...message, + createdBy: Pick(message.createdBy, ["id", "userid", "username", "icon"]), + })), + }); + } catch (err) { + logger.error("Database Error:", err); + + return res.code(500).send(DatabaseError()); + } + }); +} \ No newline at end of file diff --git a/packages/backend/src/routes/message/send.ts b/packages/backend/src/routes/message/send.ts new file mode 100644 index 0000000..dcdc054 --- /dev/null +++ b/packages/backend/src/routes/message/send.ts @@ -0,0 +1,39 @@ +import { DatabaseError, InputError } from "@/errors"; +import Logger from "@/lib/logger"; +import { MessageEntity } from "@/modules/entities/Message"; +import { MessageRepository } from "@/modules/repositories/Message"; +import type { FastifyInstance } from "fastify"; + +export default async function MessageSend(fastify: FastifyInstance) { + const logger = new Logger("Endpoint | message/send"); + + fastify.post("/", async (req, res) => { + res.header("Content-Type", "application/json"); + res.header("Connection", "close"); + + if ("error" in req.token) + return res.code(400).send(req.token); + + const result = MessageRepository.schema + .omit({ userid: true }) + .safeParse(req.body); + + if (!result.success) { + return res.code(400).send(InputError(result.error.issues)); + } + + try { + const messageRepo = fastify.orm.em.getRepository(MessageEntity); + const id = await messageRepo.sendMessage(result.data.message, result.data.channel, req.token.user.id); + + return res.send({ + success: true, + id, + }); + } catch (err) { + logger.error("Database Error:", err); + + return res.code(500).send(DatabaseError()); + } + }); +} \ No newline at end of file diff --git a/packages/frontend/index.html b/packages/frontend/index.html index 9a6d8dd..a1a553f 100755 --- a/packages/frontend/index.html +++ b/packages/frontend/index.html @@ -18,12 +18,12 @@ width: 100vw; height: 100vh; font-size: 16px; - background-color: #ffffff; + background-color: #f0f0f0; } @media (prefers-color-scheme: dark) { html, body { - background-color: #1b1b1b; + background-color: #181818; } } diff --git a/packages/frontend/src/Layout.vue b/packages/frontend/src/Layout.vue index a886fb8..54f42e0 100755 --- a/packages/frontend/src/Layout.vue +++ b/packages/frontend/src/Layout.vue @@ -5,7 +5,9 @@ :size="6" /> -
+
+
+
@@ -65,6 +67,22 @@ + + \ No newline at end of file diff --git a/packages/frontend/src/components/Modal/Confirm.vue b/packages/frontend/src/components/Modal/Confirm.vue new file mode 100644 index 0000000..1951975 --- /dev/null +++ b/packages/frontend/src/components/Modal/Confirm.vue @@ -0,0 +1,60 @@ + + + + + \ No newline at end of file diff --git a/packages/frontend/src/components/Modal/Error.vue b/packages/frontend/src/components/Modal/Error.vue index c69f760..dd73e56 100644 --- a/packages/frontend/src/components/Modal/Error.vue +++ b/packages/frontend/src/components/Modal/Error.vue @@ -16,6 +16,13 @@ color="accent" @click='$emit("PleaseClose")' /> + +
@@ -46,4 +53,6 @@ defineProps<{ error: any; canClose: boolean; }>(); + +const reload = () => window.location.reload(); \ No newline at end of file diff --git a/packages/frontend/src/components/Modal/TopNotice.vue b/packages/frontend/src/components/Modal/TopNotice.vue new file mode 100644 index 0000000..2637ec4 --- /dev/null +++ b/packages/frontend/src/components/Modal/TopNotice.vue @@ -0,0 +1,75 @@ + + + + + \ No newline at end of file diff --git a/packages/frontend/src/global.css b/packages/frontend/src/global.css index c023684..31476b8 100755 --- a/packages/frontend/src/global.css +++ b/packages/frontend/src/global.css @@ -1,4 +1,4 @@ -@import url("https://fonts.googleapis.com/css2?family=BIZ+UDPGothic&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=BIZ+UDPGothic:wght@400;700&display=swap"); :root { --bg-color: #f0f0f0; @@ -8,6 +8,7 @@ --text-sub-color: #595959; --border-color: #e0e0e0; --error-color: #ff0000; + --warn-color: #ff9d00; --success-color: #00ff00; --accent-color: #3ad0a1; --accent-in-text-color: #000000; diff --git a/packages/frontend/src/lib/modal.ts b/packages/frontend/src/lib/modal.ts index f6c61f0..b27000e 100644 --- a/packages/frontend/src/lib/modal.ts +++ b/packages/frontend/src/lib/modal.ts @@ -1,37 +1,46 @@ +import TopNotice from "@/components/Modal/TopNotice.vue"; import { createApp, h, ref, type Component } from "vue"; -const layer = ref(0); +const layer = ref(1); export const createModal = (data: { component: T, + isTopNotice?: boolean; style?: Record, props?: Record any) | any>, - onClose?: () => void + onClose?: (...args: any) => any }) => { - layer.value++ - const newContainer = document.createElement("div"); - for (const [key, value] of Object.entries({ - ...data.style, - transform: `translateZ(${layer.value})`, - position: "fixed", - inset: "0", - display: "flex", - "justify-content": "center", - "align-items": "center", - width: "100dvw", - height: "100dvh", - "background-color": "#00000080", - })) { - newContainer.style.setProperty(key, value); - } + layer.value++; - const container = document.querySelector(".modals-container")! - .appendChild(newContainer); + let container: HTMLDivElement; + if (!data.isTopNotice) { + const newContainer = document.createElement("div"); + for (const [key, value] of Object.entries({ + ...data.style, + "transform": `translateZ(${layer.value})`, + "position": "fixed", + "inset": "0", + "display": "flex", + "justify-content": "center", + "align-items": "center", + "width": "100dvw", + "height": "100dvh", + "background-color": "#00000080", + })) { + newContainer.style.setProperty(key, value); + } + + container = document.querySelector(".modals-container")! + .appendChild(newContainer); + } else { + container = document.querySelector(".top-notice-container")! + .appendChild(document.createElement("div")); + } let app: ReturnType; - const close = () => { - data.onClose?.(); + const close = (closeData?: any) => { + data.onClose?.(closeData); layer.value--; app.unmount(); container.remove(); @@ -49,4 +58,14 @@ export const createModal = (data: { app.mount(container); return close; -} \ No newline at end of file +} + +export const createTopNotice = (data?: { + style?: Record, + props?: Record any) | any>, + onClose?: (...args: any) => any +}) => createModal({ + ...data, + component: TopNotice, + isTopNotice: true, +}); \ No newline at end of file diff --git a/packages/frontend/src/lib/parser.ts b/packages/frontend/src/lib/parser.ts new file mode 100644 index 0000000..0aeb83f --- /dev/null +++ b/packages/frontend/src/lib/parser.ts @@ -0,0 +1,28 @@ +export function DateParse(date: string | Date, startDate: Date = new Date()) { + const diffMs = startDate.getTime() - new Date(date).getTime(); + const diffSec = Math.abs(Math.floor(diffMs / 1000)); + const diffMin = Math.abs(Math.floor(diffSec / 60)); + const diffHour = Math.abs(Math.floor(diffMin / 60)); + const diffDay = Math.abs(Math.floor(diffHour / 24)); + const diffMonth = Math.abs(Math.floor(diffDay / 30)); + const diffYear = Math.abs(Math.floor(diffMonth / 12)); + + const diffStr = diffMs < 0 + ? "後" + : "前"; + + switch (true) { + case diffSec < 60: + return `${diffSec}秒${diffStr}`; + case diffMin < 60: + return `${diffMin}分${diffStr}`; + case diffHour < 24: + return `${diffHour}時間${diffStr}`; + case diffDay < 30: + return `${diffDay}日${diffStr}`; + case diffMonth < 12: + return `${diffMonth}ヶ月${diffStr}`; + default: + return `${diffYear}年${diffStr}`; + } +} \ No newline at end of file diff --git a/packages/frontend/src/lib/sw.ts b/packages/frontend/src/lib/sw.ts index accd17f..8170517 100644 --- a/packages/frontend/src/lib/sw.ts +++ b/packages/frontend/src/lib/sw.ts @@ -1,27 +1,63 @@ const swSelf = globalThis as unknown as ServiceWorkerGlobalScope; +const resources = { + "general": [ + "/assets/lynqchat.svg", + ], +}; + +// @ts-expect-error +const manifest = self.__WB_MANIFEST || []; +manifest.forEach((entry: any) => { + const url = typeof entry === "string" + ? entry + : entry.url; + resources.general.push(url); +}); + swSelf.addEventListener("install", (event) => { - event.waitUntil(swSelf.skipWaiting()); + swSelf.skipWaiting(); + + event.waitUntil((async () => { + await Promise.all(Object.entries(resources).map(async ([name, assets]) => { + const cache = await caches.open(name); + + for (const asset of assets) { + try { + await cache.add(asset); + } catch (error) { + console.error(`Failed to cache asset: ${asset}`, error); + } + } + })); + })()); }); swSelf.addEventListener("activate", (event) => { - event.waitUntil(swSelf.clients.claim()); + event.waitUntil((async () => { + await swSelf.clients.claim(); + })()); }); swSelf.addEventListener("fetch", (event) => { const request = event.request; - if (request.url.indexOf("http") !== 0) + if (request.method !== "GET") return; - + + const url = new URL(request.url); + if (url.origin !== location.origin) + return; + event.respondWith((async () => { + const cached = await caches.match(request); + if (cached) + return cached; + try { - const res = await fetch(request); - return res; - } catch (err) { - return new Response("Network error", { - status: 504, - }); + return await fetch(request); + } catch (error) { + return new Response("Network error", { status: 504 }); } })()); }); \ No newline at end of file diff --git a/packages/frontend/src/main.ts b/packages/frontend/src/main.ts index a9ea957..2bf8558 100755 --- a/packages/frontend/src/main.ts +++ b/packages/frontend/src/main.ts @@ -75,7 +75,7 @@ const router = createRouter({ ], }); router.beforeEach((to, from) => { - if (to.path === from.path) + if (from.matched.length > 0 && to.path === from.path) return false; routerStatus.isLoad = true; diff --git a/packages/frontend/src/routes/community/channel.vue b/packages/frontend/src/routes/community/channel.vue index f909a3c..baebdf1 100644 --- a/packages/frontend/src/routes/community/channel.vue +++ b/packages/frontend/src/routes/community/channel.vue @@ -9,14 +9,114 @@ v-if="!isProcessing && channel" class="channel" > +
+
+ 何ということでしょう!! + メッセージがありませんね!! +
+ + + + +
+ +
+