diff --git a/.gitignore b/.gitignore index 79518f7..cb20f44 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* + +http diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..6b9a518 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/lib/db/schema.ts', + out: './src/lib/db/migrations', + dialect: 'postgresql', + dbCredentials: { + url: process.env.SECRET_XATA_PG_ENDPOINT! + }, + verbose: true, + strict: true +}); diff --git a/package-lock.json b/package-lock.json index 35ed689..91978d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,13 +23,14 @@ "pg": "^8.13.1" }, "devDependencies": { + "@internationalized/date": "^3.6.0", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0", "@types/eslint": "^9.6.0", "@types/pg": "^8.11.10", "autoprefixer": "^10.4.20", - "bits-ui": "^1.0.0-next.64", + "bits-ui": "^1.0.0-next.66", "clsx": "^2.1.1", "drizzle-kit": "^0.25.0", "eslint": "^9.7.0", @@ -2215,9 +2216,9 @@ } }, "node_modules/bits-ui": { - "version": "1.0.0-next.64", - "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.0.0-next.64.tgz", - "integrity": "sha512-r1JThjUSKwTkaB1onwIs7ZQoqygSsWhjBaUElCS8m8CCbY1RxmTz0HnbN+Xp2oJgJ4YQgIfiXTG3170l80FEgg==", + "version": "1.0.0-next.66", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.0.0-next.66.tgz", + "integrity": "sha512-2cvX2dhtlp79dq3D79sw/e/w2tib+ySI4RScXwSK9JLSOLVWgVM6XvnT3g8dYRSsSCnQ+hlTZ1ydJJc209HiZw==", "dev": true, "dependencies": { "@floating-ui/core": "^1.6.4", diff --git a/package.json b/package.json index 9c56380..8157e44 100644 --- a/package.json +++ b/package.json @@ -12,18 +12,19 @@ "lint": "prettier --check . && eslint .", "test:unit": "vitest", "test": "npm run test:unit -- --run", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio" + "db:generate": "drizzle-kit --config=drizzle.config.ts generate", + "db:migrate": "drizzle-kit --config=drizzle.config.ts migrate", + "db:studio": "drizzle-kit --config=drizzle.config.ts studio" }, "devDependencies": { + "@internationalized/date": "^3.6.0", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0", "@types/eslint": "^9.6.0", "@types/pg": "^8.11.10", "autoprefixer": "^10.4.20", - "bits-ui": "^1.0.0-next.64", + "bits-ui": "^1.0.0-next.66", "clsx": "^2.1.1", "drizzle-kit": "^0.25.0", "eslint": "^9.7.0", diff --git a/src/lib/api/api.ts b/src/lib/api/api.ts index afd9521..c802b52 100644 --- a/src/lib/api/api.ts +++ b/src/lib/api/api.ts @@ -11,6 +11,7 @@ import { userRouter } from './user/user.controller'; import { projectsRouter } from './projects/projects.controller'; import { websitesRouter } from './websites/websites.controller'; import { statusRouter } from './status/status.controller'; +import { messagesRouter } from './messages/messages.controller'; export const api = new Hono().basePath('/api'); @@ -21,6 +22,7 @@ api.route('/user', userRouter); api.route('/projects', projectsRouter); api.route('/websites', websitesRouter); api.route('/status', statusRouter); +api.route('/messages', messagesRouter); function getAuthConfig(): AuthConfig { return { diff --git a/src/lib/api/messages/messages.controller.ts b/src/lib/api/messages/messages.controller.ts new file mode 100644 index 0000000..6591739 --- /dev/null +++ b/src/lib/api/messages/messages.controller.ts @@ -0,0 +1,126 @@ +import { verifyAuth } from '@hono/auth-js'; +import { Hono, type Context } from 'hono'; +import { + createMessage, + deleteMessage, + getMessage, + getMessages, + updateMessage +} from './messages.service'; +import { validateRequestBody } from '../middlewares'; +import { InsertMessageSchema } from '$lib/db/schema'; + +export const messagesRouter = new Hono(); + +export const getMessageController = async (context: Context) => { + const { token } = context.get('authUser'); + if (!token) return context.status(401); + + const { messageId } = context.req.param(); + if (!messageId) return context.json({ error: 'Missing message Id' }, 400); + + const messageResponse = await getMessage(String(token.id), messageId); + + return context.json( + messageResponse.error ? { error: messageResponse.error } : messageResponse.data, + messageResponse.status + ); +}; + +export const getMessagesController = async (context: Context) => { + const { token } = context.get('authUser'); + if (!token) return context.status(401); + + const { websiteId } = context.req.query(); + + const messagesResponse = await getMessages(String(token.id), websiteId); + + return context.json( + messagesResponse.error ? { error: messagesResponse.error } : messagesResponse.data, + messagesResponse.status + ); +}; + +export const postMessageController = async (context: Context) => { + const { token } = context.get('authUser'); + if (!token) return context.status(401); + + const { websiteId } = context.req.param(); + if (!websiteId) return context.json({ error: 'Missing website ID' }, 400); + + const message = context.get('requestBody'); + + const createWebsiteResponse = await createMessage({ + userId: String(token.id), + websiteId: websiteId, + ...message + }); + + return context.json( + createWebsiteResponse.error + ? { error: createWebsiteResponse.error } + : createWebsiteResponse.data, + createWebsiteResponse.status + ); +}; + +export const putMessageController = async (context: Context) => { + const { token } = context.get('authUser'); + if (!token) return context.status(401); + + const { websiteId } = context.req.param(); + if (!websiteId) return context.json({ error: 'Missing website ID' }, 400); + + const { messageId } = context.req.param(); + if (!messageId) return context.json({ error: 'Missing message ID' }, 400); + + const updatedMessage = context.get('requestBody'); + + const updateWebsiteResponse = await updateMessage(String(token.id), messageId, updatedMessage); + + return context.json( + updateWebsiteResponse.error + ? { error: updateWebsiteResponse.error } + : updateWebsiteResponse.data, + updateWebsiteResponse.status + ); +}; + +export const deleteMessageController = async (context: Context) => { + const { token } = context.get('authUser'); + if (!token) return context.status(401); + + const { messageId } = context.req.param(); + if (!messageId) return context.json({ error: 'Missing message ID' }, 400); + + const deleteMessageResponse = await deleteMessage(String(token.id), messageId); + + return context.json( + deleteMessageResponse.error + ? { error: deleteMessageResponse.error } + : deleteMessageResponse.data, + + deleteMessageResponse.status + ); +}; + +const PartialInsertMessageSchema = InsertMessageSchema.pick({ + title: true, + content: true, + startTime: true +}); + +messagesRouter.use(verifyAuth()); +messagesRouter.get('/:messageId', getMessageController); +messagesRouter.get('/', getMessagesController); +messagesRouter.post( + '/:websiteId', + validateRequestBody(PartialInsertMessageSchema), + postMessageController +); +messagesRouter.put( + '/:websiteId/:messageId', + validateRequestBody(PartialInsertMessageSchema), + putMessageController +); +messagesRouter.delete('/:messageId', deleteMessageController); diff --git a/src/lib/api/messages/messages.service.ts b/src/lib/api/messages/messages.service.ts new file mode 100644 index 0000000..f0e4e12 --- /dev/null +++ b/src/lib/api/messages/messages.service.ts @@ -0,0 +1,178 @@ +import { db } from '$lib/db/drizzle'; +import { messages, websites, type InsertMessage, type SelectMessagePartial } from '$lib/db/schema'; +import { and, count, eq } from 'drizzle-orm'; +import type { ServiceResponse } from '../types'; +import type { StatusCode } from 'hono/utils/http-status'; +import { isUserPro } from '../user/user.service'; +import { prettifyErrors } from '$lib/db/utils'; + +export const canWbsiteHaveMoreMessages = async ( + userId: string, + projectId: string +): Promise => { + if (await isUserPro(userId)) return true; + + return (await getMessagesCount(projectId)) < 5; +}; + +export const getMessagesCount = async (websiteId: string): Promise => { + return await db + .select({ count: count(messages.id) }) + .from(messages) + .where(eq(messages.websiteId, websiteId)) + .then((response) => response[0].count); +}; + +export const getMessage = async ( + userId: string, + messageId: string +): Promise> => { + return await db + .select({ + id: messages.id, + title: messages.title, + content: messages.content, + startTime: messages.startTime + }) + .from(messages) + .where(and(eq(messages.userId, userId), eq(messages.id, messageId))) + .limit(1) + .then((response) => { + if (response.length === 0) + return { + status: 404 as StatusCode, + error: 'Message not found' + }; + + return { + status: 200 as StatusCode, + data: response[0] + }; + }) + .catch((error) => { + return { + status: 400 as StatusCode, + error: error.message + }; + }); +}; + +export const getMessages = async ( + userId: string, + websiteId?: string +): Promise> => { + const condition = websiteId + ? and(eq(messages.userId, userId), eq(messages.websiteId, websiteId)) + : eq(messages.userId, userId); + return await db + .select({ + id: messages.id, + title: messages.title, + content: messages.content, + websiteId: messages.websiteId, + startTime: messages.startTime + }) + .from(messages) + .where(condition) + .then((response) => { + return { + status: 200 as StatusCode, + data: response + }; + }) + .catch((error) => { + return { + status: 400 as StatusCode, + error: error.message + }; + }); +}; + +export const createMessage = async ( + message: InsertMessage +): Promise> => { + if (!(await canWbsiteHaveMoreMessages(message.userId, message.websiteId))) + return { + status: 403, + error: 'Hobby users can create 5 messages per website!' + }; + + return await db + .insert(messages) + .values(message) + .returning({ + id: messages.id, + title: messages.title, + content: messages.content, + startTime: messages.startTime + }) + .then((response) => { + return { + status: 200 as StatusCode, + data: response[0] + }; + }) + .catch((error) => { + return { + status: 400 as StatusCode, + error: prettifyErrors(error) + }; + }); +}; + +export const updateMessage = async ( + userId: string, + messageId: string, + updatedMessage: InsertMessage +): Promise> => { + return await db + .update(messages) + .set(updatedMessage) + .where(and(eq(websites.userId, userId), eq(messages.id, messageId))) + .returning({ + id: messages.id, + title: messages.title, + content: messages.content, + startTime: messages.startTime + }) + .then((response) => { + return { + status: 200 as StatusCode, + data: response[0] + }; + }) + .catch((error) => { + return { + status: 400 as StatusCode, + error: error.message + }; + }); +}; + +export const deleteMessage = async ( + userId: string, + messageId: string +): Promise> => { + return await db + .delete(messages) + .where(and(eq(messages.userId, userId), eq(messages.id, messageId))) + .returning({ deletedId: websites.id }) + .then((response) => { + if (response.length === 0) { + return { + status: 404 as StatusCode, + error: 'Message not found' + }; + } + return { + status: 200 as StatusCode, + data: response[0] + }; + }) + .catch((error) => { + return { + status: 400 as StatusCode, + error: error.message + }; + }); +}; diff --git a/src/lib/api/status/status.service.ts b/src/lib/api/status/status.service.ts index 898c24e..6f9926d 100644 --- a/src/lib/api/status/status.service.ts +++ b/src/lib/api/status/status.service.ts @@ -1,5 +1,6 @@ import { db } from '$lib/db/drizzle'; import { + messages, projects, uptimeChecks, websites, @@ -7,7 +8,7 @@ import { type SelectProjectPartial } from '$lib/db/schema'; import { desc, eq } from 'drizzle-orm'; -import type { ServiceResponse, StatusPageResponse } from '../types'; +import type { ServiceResponse, StatusPageMessages, StatusPageResponse } from '../types'; import type { StatusCode } from 'hono/utils/http-status'; export const getStatus = async ( @@ -40,6 +41,22 @@ export const getStatus = async ( }); }; +export const getMessages = async (websiteId: string): Promise => { + return await db + .select({ + title: messages.title, + content: messages.content, + startTime: messages.startTime + }) + .from(messages) + .where(eq(messages.websiteId, websiteId)) + .orderBy(desc(messages.startTime)) + .catch((error) => { + console.log(error); + return []; + }); +}; + export const getStatusByProject = async ( projectSlug: string ): Promise> => { @@ -90,15 +107,16 @@ export const getStatusPage = async ( const statusPromises = websitesResponse.map(async (website) => { const statusResponse = await getStatus(website.id, 50); - if (statusResponse.status !== 200 || !statusResponse.data) { - // Optionally handle individual errors here - return null; // Exclude this website from the results + return null; } + const messages = await getMessages(website.id); + return { ...website, - statuses: statusResponse.data + statuses: statusResponse.data, + messages: messages }; }); diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts index b753022..044eee1 100644 --- a/src/lib/api/types.ts +++ b/src/lib/api/types.ts @@ -1,7 +1,11 @@ -import type { SelectPartialStatus, SelectWebsitePartial } from '$lib/db/schema'; +import type { SelectMessage, SelectPartialStatus, SelectWebsitePartial } from '$lib/db/schema'; import type { StatusCode } from 'hono/utils/http-status'; -export type StatusPageResponse = SelectWebsitePartial & { statuses: SelectPartialStatus[] }; +export type StatusPageMessages = Pick; +export type StatusPageResponse = SelectWebsitePartial & { + statuses: SelectPartialStatus[]; + messages: StatusPageMessages[]; +}; export type ServiceResponse = { data?: T; diff --git a/src/lib/api/websites/websites.controller.ts b/src/lib/api/websites/websites.controller.ts index 91f2061..5116062 100644 --- a/src/lib/api/websites/websites.controller.ts +++ b/src/lib/api/websites/websites.controller.ts @@ -5,6 +5,7 @@ import { deleteWebsite, getWebsite, getWebsites, + getWebsitesForUser, updateWebsite } from './websites.service'; import { validateRequestBody } from '../middlewares'; @@ -34,6 +35,18 @@ export const getWebsiteController = async (context: Context) => { ); }; +export const getWebsitesForUserController = async (context: Context) => { + const { token } = context.get('authUser'); + if (!token) return context.status(401); + + const websiteResponse = await getWebsitesForUser(String(token.id)); + + return context.json( + websiteResponse.error ? { error: websiteResponse.error } : websiteResponse.data, + websiteResponse.status + ); +}; + export const getWebsitesController = async (context: Context) => { const { token } = context.get('authUser'); if (!token) return context.status(401); @@ -121,8 +134,9 @@ const PartialInsertWebsiteSchema = InsertWebsiteSchema.pick({ }); websitesRouter.use(verifyAuth()); -websitesRouter.get('/:slug/:websiteId', getWebsiteController); +websitesRouter.get('/', getWebsitesForUserController); websitesRouter.get('/:slug', getWebsitesController); +websitesRouter.get('/:slug/:websiteId', getWebsiteController); websitesRouter.post( '/:slug', validateRequestBody(PartialInsertWebsiteSchema), diff --git a/src/lib/api/websites/websites.service.ts b/src/lib/api/websites/websites.service.ts index 98fdc70..2b485c3 100644 --- a/src/lib/api/websites/websites.service.ts +++ b/src/lib/api/websites/websites.service.ts @@ -1,5 +1,5 @@ import { db } from '$lib/db/drizzle'; -import { websites, type InsertWebsite, type SelectWebsitePartial } from '$lib/db/schema'; +import { projects, websites, type InsertWebsite, type SelectWebsitePartial } from '$lib/db/schema'; import { and, count, eq } from 'drizzle-orm'; import type { ServiceResponse } from '../types'; import type { StatusCode } from 'hono/utils/http-status'; @@ -89,6 +89,42 @@ export const getWebsites = async ( }); }; +export const getWebsitesForUser = async ( + userId: string +): Promise< + ServiceResponse< + { + id: string; + name: string; + projectId: string; + projectName: string | null; + }[] + > +> => { + return await db + .select({ + id: websites.id, + name: websites.name, + projectId: websites.projectId, + projectName: projects.name + }) + .from(websites) + .leftJoin(projects, eq(websites.projectId, projects.id)) + .where(eq(websites.userId, userId)) + .then((response) => { + return { + status: 200 as StatusCode, + data: response + }; + }) + .catch((error) => { + return { + status: 400 as StatusCode, + error: error.message + }; + }); +}; + export const createWebsite = async ( website: InsertWebsite ): Promise> => { diff --git a/src/lib/components/ui/accordion/accordion-content.svelte b/src/lib/components/ui/accordion/accordion-content.svelte new file mode 100644 index 0000000..f2cfe88 --- /dev/null +++ b/src/lib/components/ui/accordion/accordion-content.svelte @@ -0,0 +1,24 @@ + + + +
+ {@render children?.()} +
+
diff --git a/src/lib/components/ui/accordion/accordion-item.svelte b/src/lib/components/ui/accordion/accordion-item.svelte new file mode 100644 index 0000000..dcffa3c --- /dev/null +++ b/src/lib/components/ui/accordion/accordion-item.svelte @@ -0,0 +1,12 @@ + + + diff --git a/src/lib/components/ui/accordion/accordion-trigger.svelte b/src/lib/components/ui/accordion/accordion-trigger.svelte new file mode 100644 index 0000000..8753749 --- /dev/null +++ b/src/lib/components/ui/accordion/accordion-trigger.svelte @@ -0,0 +1,29 @@ + + + + svg]:rotate-180', + className + )} + {...restProps} + > + {@render children?.()} + + + diff --git a/src/lib/components/ui/accordion/index.ts b/src/lib/components/ui/accordion/index.ts new file mode 100644 index 0000000..3fc36e7 --- /dev/null +++ b/src/lib/components/ui/accordion/index.ts @@ -0,0 +1,17 @@ +import { Accordion as AccordionPrimitive } from "bits-ui"; +import Content from "./accordion-content.svelte"; +import Item from "./accordion-item.svelte"; +import Trigger from "./accordion-trigger.svelte"; + +const Root = AccordionPrimitive.Root; +export { + Root, + Content, + Item, + Trigger, + // + Root as Accordion, + Content as AccordionContent, + Item as AccordionItem, + Trigger as AccordionTrigger, +}; diff --git a/src/lib/components/ui/app-sidebar/app-sidebar.svelte b/src/lib/components/ui/app-sidebar/app-sidebar.svelte index 9f19c7a..2e615cb 100644 --- a/src/lib/components/ui/app-sidebar/app-sidebar.svelte +++ b/src/lib/components/ui/app-sidebar/app-sidebar.svelte @@ -59,6 +59,13 @@ {/snippet} + + + {#snippet child({ props })} + Messages + {/snippet} + + {#snippet child({ props })} diff --git a/src/lib/components/ui/calendar/calendar-cell.svelte b/src/lib/components/ui/calendar/calendar-cell.svelte new file mode 100644 index 0000000..8f3f766 --- /dev/null +++ b/src/lib/components/ui/calendar/calendar-cell.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/ui/calendar/calendar-day.svelte b/src/lib/components/ui/calendar/calendar-day.svelte new file mode 100644 index 0000000..e9be9c0 --- /dev/null +++ b/src/lib/components/ui/calendar/calendar-day.svelte @@ -0,0 +1,31 @@ + + + diff --git a/src/lib/components/ui/calendar/calendar-grid-body.svelte b/src/lib/components/ui/calendar/calendar-grid-body.svelte new file mode 100644 index 0000000..8cd86de --- /dev/null +++ b/src/lib/components/ui/calendar/calendar-grid-body.svelte @@ -0,0 +1,12 @@ + + + diff --git a/src/lib/components/ui/calendar/calendar-grid-head.svelte b/src/lib/components/ui/calendar/calendar-grid-head.svelte new file mode 100644 index 0000000..333edc4 --- /dev/null +++ b/src/lib/components/ui/calendar/calendar-grid-head.svelte @@ -0,0 +1,12 @@ + + + diff --git a/src/lib/components/ui/calendar/calendar-grid-row.svelte b/src/lib/components/ui/calendar/calendar-grid-row.svelte new file mode 100644 index 0000000..9032236 --- /dev/null +++ b/src/lib/components/ui/calendar/calendar-grid-row.svelte @@ -0,0 +1,12 @@ + + + diff --git a/src/lib/components/ui/calendar/calendar-grid.svelte b/src/lib/components/ui/calendar/calendar-grid.svelte new file mode 100644 index 0000000..1d7edb5 --- /dev/null +++ b/src/lib/components/ui/calendar/calendar-grid.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/ui/calendar/calendar-head-cell.svelte b/src/lib/components/ui/calendar/calendar-head-cell.svelte new file mode 100644 index 0000000..4e75040 --- /dev/null +++ b/src/lib/components/ui/calendar/calendar-head-cell.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/ui/calendar/calendar-header.svelte b/src/lib/components/ui/calendar/calendar-header.svelte new file mode 100644 index 0000000..e64feae --- /dev/null +++ b/src/lib/components/ui/calendar/calendar-header.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/ui/calendar/calendar-heading.svelte b/src/lib/components/ui/calendar/calendar-heading.svelte new file mode 100644 index 0000000..5d57a50 --- /dev/null +++ b/src/lib/components/ui/calendar/calendar-heading.svelte @@ -0,0 +1,12 @@ + + + diff --git a/src/lib/components/ui/calendar/calendar-months.svelte b/src/lib/components/ui/calendar/calendar-months.svelte new file mode 100644 index 0000000..4cd0ed7 --- /dev/null +++ b/src/lib/components/ui/calendar/calendar-months.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/calendar/calendar-next-button.svelte b/src/lib/components/ui/calendar/calendar-next-button.svelte new file mode 100644 index 0000000..3eaff6f --- /dev/null +++ b/src/lib/components/ui/calendar/calendar-next-button.svelte @@ -0,0 +1,28 @@ + + +{#snippet Fallback()} + +{/snippet} + + diff --git a/src/lib/components/ui/calendar/calendar-prev-button.svelte b/src/lib/components/ui/calendar/calendar-prev-button.svelte new file mode 100644 index 0000000..77430c9 --- /dev/null +++ b/src/lib/components/ui/calendar/calendar-prev-button.svelte @@ -0,0 +1,28 @@ + + +{#snippet Fallback()} + +{/snippet} + + diff --git a/src/lib/components/ui/calendar/calendar.svelte b/src/lib/components/ui/calendar/calendar.svelte new file mode 100644 index 0000000..16bebd7 --- /dev/null +++ b/src/lib/components/ui/calendar/calendar.svelte @@ -0,0 +1,61 @@ + + + + + {#snippet children({ months, weekdays })} + + + + + + + {#each months as month} + + + + {#each weekdays as weekday} + + {weekday.slice(0, 2)} + + {/each} + + + + {#each month.weeks as weekDates} + + {#each weekDates as date} + + + + {/each} + + {/each} + + + {/each} + + {/snippet} + diff --git a/src/lib/components/ui/calendar/index.ts b/src/lib/components/ui/calendar/index.ts new file mode 100644 index 0000000..ab257ab --- /dev/null +++ b/src/lib/components/ui/calendar/index.ts @@ -0,0 +1,30 @@ +import Root from "./calendar.svelte"; +import Cell from "./calendar-cell.svelte"; +import Day from "./calendar-day.svelte"; +import Grid from "./calendar-grid.svelte"; +import Header from "./calendar-header.svelte"; +import Months from "./calendar-months.svelte"; +import GridRow from "./calendar-grid-row.svelte"; +import Heading from "./calendar-heading.svelte"; +import GridBody from "./calendar-grid-body.svelte"; +import GridHead from "./calendar-grid-head.svelte"; +import HeadCell from "./calendar-head-cell.svelte"; +import NextButton from "./calendar-next-button.svelte"; +import PrevButton from "./calendar-prev-button.svelte"; + +export { + Day, + Cell, + Grid, + Header, + Months, + GridRow, + Heading, + GridBody, + GridHead, + HeadCell, + NextButton, + PrevButton, + // + Root as Calendar, +}; diff --git a/src/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte index 08c3faf..37ef6c9 100644 --- a/src/lib/components/ui/input/input.svelte +++ b/src/lib/components/ui/input/input.svelte @@ -1,5 +1,6 @@
-
- {label} - - {#if restProps['required']}*{/if} - +
+
+ {label} + + {#if restProps['required']}*{/if} + +
+
- + {#if restProps['type'] === 'time'} +
+ +
+ {/if}
+ + diff --git a/src/lib/components/ui/messages/message-delete.svelte b/src/lib/components/ui/messages/message-delete.svelte new file mode 100644 index 0000000..e653c44 --- /dev/null +++ b/src/lib/components/ui/messages/message-delete.svelte @@ -0,0 +1,39 @@ + + +{#if selectedMessage} +
+
+ Delete - {selectedMessage.title} ? +
+ +
+{/if} diff --git a/src/lib/components/ui/messages/message-form.svelte b/src/lib/components/ui/messages/message-form.svelte new file mode 100644 index 0000000..60088fd --- /dev/null +++ b/src/lib/components/ui/messages/message-form.svelte @@ -0,0 +1,185 @@ + + +
+ {#if formError} +
+
+ + {formError} +
+
+ {/if} + + + {triggerContent} + + + + {#each websiteOptions as websiteOption} + + {websiteOption.label} + + {/each} + + + + + +
+
+ + + {#snippet child({ props })} + + {/snippet} + + + + + +
+
+ +
+
+