Daisy UI #14

Merged
BenjaminPalko merged 28 commits from daisy-ui into master 2024-12-19 21:20:21 -05:00
69 changed files with 663 additions and 449 deletions

3
.gitignore vendored
View file

@ -17,4 +17,5 @@ Thumbs.db
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
*storybook.log */dev.db
*/dev.db-journal

View file

@ -1,8 +1,14 @@
{ {
"useTabs": true, "useTabs": true,
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "es5",
"printWidth": 100, "printWidth": 100,
piopi commented 2024-12-19 21:02:26 -05:00 (Migrated from github.com)
Review

🙈

🙈
"endOfLine": "lf",
"arrowParens": "always",
"jsxSingleQuote": false,
"semi": true,
"quoteProps": "as-needed",
"tabWidth": 4,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [ "overrides": [
{ {

View file

@ -2,15 +2,16 @@
const config = { const config = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|ts|svelte)'], stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|ts|svelte)'],
addons: [ addons: [
'@storybook/addon-svelte-csf',
'@storybook/addon-essentials',
'@chromatic-com/storybook', '@chromatic-com/storybook',
'@storybook/addon-essentials',
'@storybook/addon-interactions', '@storybook/addon-interactions',
'storybook-dark-mode' '@storybook/addon-styling-webpack',
'@storybook/addon-svelte-csf',
'@storybook/addon-themes',
], ],
framework: { framework: {
name: '@storybook/sveltekit', name: '@storybook/sveltekit',
options: {} options: {},
} },
}; };
export default config; export default config;

4
.storybook/preview.css Normal file
View file

@ -0,0 +1,4 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import '@flaticon/flaticon-uicons/css/all/all';

View file

@ -1,13 +1,28 @@
import { withThemeByDataAttribute } from '@storybook/addon-themes';
import './preview.css';
/** @type { import('@storybook/svelte').Preview } */ /** @type { import('@storybook/svelte').Preview } */
const preview = { const preview = {
tags: ['autodocs'],
parameters: { parameters: {
controls: { controls: {
matchers: { matchers: {
color: /(background|color)$/i, color: /(background|color)$/i,
date: /Date$/i date: /Date$/i,
} },
} },
} },
decorators: [
withThemeByDataAttribute({
themes: {
light: 'light',
dark: 'dark',
night: 'night',
},
defaultTheme: 'dark',
attributeName: 'data-theme',
}),
],
}; };
export default preview; export default preview;

View file

@ -1,38 +1,69 @@
# sv # Hestia
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). Hestia is an early stage project
## Creating a project ## Setup
If you're seeing this, you've probably already done this step. Congrats!
```bash ```bash
# create a new project in the current directory # install dependencies
bunx sv create bun install
# create a new project in my-app # set up local database
bunx sv create my-app bun prisma:dev
``` ```
## Developing ## Developing
Once you've created a project and installed dependencies with `bun install`, start a development server: Once you've created a project and installed dependencies, start a development server:
```bash ```bash
bun run dev bun dev
# or start the server and open the app in a new browser tab # or start the server and open the app in a new browser tab
bun run dev -- --open bun dev -- --open
# to use storybook for components development
bun storybook
# interact with local database
bun prisma:studio
``` ```
> You can access the Yoga web-app at `/api/graphql`
## Building ## Building
To create a production version of your app: To create a production version of your app:
```bash ```bash
bun run build bun build
``` ```
## Stack
- https://svelte.dev/docs/kit/introduction
- https://zod.dev/
- https://day.js.org/
### Frontend
- https://tailwindcss.com/
- https://www.flaticon.com/
- https://daisyui.com/
### Backend
- https://www.prisma.io/
- https://pothos-graphql.dev/
- https://the-guild.dev/graphql/yoga-server
- https://github.com/pinojs/pino
### Tools
- https://storybook.js.org/
- https://vite.dev/
- https://vitest.dev/
You can preview the production build with `bun run preview`. You can preview the production build with `bun run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

BIN
bun.lockb

Binary file not shown.

3
docs/DAISY.md Normal file
View file

@ -0,0 +1,3 @@
# Experienced Issues
- https://github.com/saadeghi/daisyui/issues/811

View file

@ -18,17 +18,17 @@ export default ts.config(
languageOptions: { languageOptions: {
globals: { globals: {
...globals.browser, ...globals.browser,
...globals.node ...globals.node,
} },
} },
}, },
{ {
files: ['**/*.svelte'], files: ['**/*.svelte'],
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
parser: ts.parser parser: ts.parser,
} },
} },
} }
); );

View file

@ -15,7 +15,12 @@
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build", "build-storybook": "storybook build",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"prisma:generate": "prisma generate" "prisma:dev": "prisma migrate dev",
"prisma:format": "prisma format",
"prisma:generate": "prisma generate",
"prisma:push": "prisma db push",
"prisma:reset": "prisma migrate reset --force",
"prisma:studio": "prisma studio"
}, },
"devDependencies": { "devDependencies": {
"@chromatic-com/storybook": "^3.2.2", "@chromatic-com/storybook": "^3.2.2",
@ -23,7 +28,9 @@
"@playwright/test": "^1.45.3", "@playwright/test": "^1.45.3",
"@storybook/addon-essentials": "^8.4.7", "@storybook/addon-essentials": "^8.4.7",
"@storybook/addon-interactions": "^8.4.7", "@storybook/addon-interactions": "^8.4.7",
"@storybook/addon-styling-webpack": "^1.0.1",
"@storybook/addon-svelte-csf": "^5.0.0-next.13", "@storybook/addon-svelte-csf": "^5.0.0-next.13",
"@storybook/addon-themes": "^8.4.7",
"@storybook/blocks": "^8.4.7", "@storybook/blocks": "^8.4.7",
"@storybook/svelte": "^8.4.7", "@storybook/svelte": "^8.4.7",
"@storybook/sveltekit": "^8.4.7", "@storybook/sveltekit": "^8.4.7",
@ -33,6 +40,7 @@
"@sveltejs/vite-plugin-svelte": "^5.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.0",
"@types/bun": "^1.1.14", "@types/bun": "^1.1.14",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"daisyui": "^4.12.22",
"eslint": "^9.7.0", "eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0", "eslint-plugin-svelte": "^2.36.0",
@ -42,7 +50,6 @@
"prettier-plugin-tailwindcss": "^0.6.5", "prettier-plugin-tailwindcss": "^0.6.5",
"prisma": "^6.0.1", "prisma": "^6.0.1",
"storybook": "^8.4.7", "storybook": "^8.4.7",
"storybook-dark-mode": "^4.0.2",
"svelte": "^5.0.0", "svelte": "^5.0.0",
"svelte-check": "^4.0.0", "svelte-check": "^4.0.0",
"tailwindcss": "^3.4.9", "tailwindcss": "^3.4.9",
@ -52,6 +59,7 @@
"vitest": "^2.0.4" "vitest": "^2.0.4"
}, },
"dependencies": { "dependencies": {
"@flaticon/flaticon-uicons": "^3.3.1",
"@lucia-auth/adapter-prisma": "^4.0.1", "@lucia-auth/adapter-prisma": "^4.0.1",
"@pothos/core": "^4.3.0", "@pothos/core": "^4.3.0",
"@pothos/plugin-prisma": "^4.4.0", "@pothos/plugin-prisma": "^4.4.0",

View file

@ -3,8 +3,8 @@ import { defineConfig } from '@playwright/test';
export default defineConfig({ export default defineConfig({
webServer: { webServer: {
command: 'npm run build && npm run preview', command: 'npm run build && npm run preview',
port: 4173 port: 4173,
}, },
testDir: 'e2e' testDir: 'e2e',
}); });

View file

@ -1,6 +1,6 @@
export default { export default {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {} autoprefixer: {},
} },
}; };

Binary file not shown.

View file

@ -1,24 +0,0 @@
/*
Warnings:
- The primary key for the `Post` table will be changed. If it partially fails, the table could be left without primary key constraint.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Post" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"published" BOOLEAN DEFAULT false,
"authorId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Post" ("authorId", "content", "createdAt", "id", "published", "title", "updatedAt") SELECT "authorId", "content", "createdAt", "id", "published", "title", "updatedAt" FROM "Post";
DROP TABLE "Post";
ALTER TABLE "new_Post" RENAME TO "Post";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View file

@ -0,0 +1,34 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"email" TEXT,
"name" TEXT NOT NULL,
"password" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL PRIMARY KEY,
"expiresAt" DATETIME NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT NOT NULL,
CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Post" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"published" BOOLEAN DEFAULT false,
"authorId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

View file

@ -16,6 +16,7 @@ datasource db {
model User { model User {
id String @id @default(uuid()) id String @id @default(uuid())
email String? @unique email String? @unique
name String name String
password String password String
@ -28,21 +29,24 @@ model User {
model Session { model Session {
id String @id @default(uuid()) id String @id @default(uuid())
expiresAt DateTime expiresAt DateTime
user User @relation(references: [id], fields: [userId])
userId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
userId String
user User @relation(references: [id], fields: [userId])
} }
model Post { model Post {
id String @id @default(uuid()) id String @id @default(uuid())
title String title String
content String content String
published Boolean? @default(false) published Boolean? @default(false)
author User @relation(references: [id], fields: [authorId]) author User @relation(references: [id], fields: [authorId])
authorId String authorId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
} }

View file

@ -1,19 +1,8 @@
@import 'tailwindcss/base'; @import 'tailwindcss/base';
@import 'tailwindcss/components'; @import 'tailwindcss/components';
@import 'tailwindcss/utilities'; @import 'tailwindcss/utilities';
@import '@flaticon/flaticon-uicons/css/all/all';
:root { :root {
@apply text-slate-800; @apply text-base-content;
}
h1 {
@apply font-display text-4xl;
}
h2 {
@apply font-display text-3xl;
}
h3 {
@apply font-display text-2xl;
} }

3
src/app.d.ts vendored
View file

@ -2,10 +2,9 @@
// for information about these interfaces // for information about these interfaces
declare global { declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
interface Locals { interface Locals {
user: import("lucia").User | null; user: import('lucia').User | null;
session: import('lucia').Session | null; session: import('lucia').Session | null;
} }
piopi commented 2024-12-19 21:03:51 -05:00 (Migrated from github.com)
Review

Will need to remove this when we switch to Clerk, maybe add a Todo to not forget it ?

Will need to remove this when we switch to Clerk, maybe add a Todo to not forget it ?
// interface PageData {} // interface PageData {}

View file

@ -1,30 +0,0 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Button from './Button.svelte';
import { fn } from '@storybook/test';
const { Story } = defineMeta({
title: 'Button',
component: Button,
tags: ['autodocs'],
args: {
onClick: fn()
},
argTypes: {
size: {
control: 'select',
options: ['small', 'normal', 'large'],
defaultValue: 'normal'
},
backgroundColor: {
control: 'color'
},
primary: {
control: 'boolean',
defaultValue: true
}
}
});
</script>
<Story name="Default" args={{ label: 'Button', size: 'normal', primary: true }} />

View file

@ -1,51 +0,0 @@
<script lang="ts">
import type { HTMLButtonAttributes } from 'svelte/elements';
let {
type = 'button',
onClick,
label,
size = 'normal',
backgroundColor,
primary = false
}: {
type?: HTMLButtonAttributes['type'];
onClick?: () => void;
label: string;
size?: 'small' | 'normal' | 'large';
backgroundColor?: string;
primary?: boolean;
} = $props();
</script>
<button
{type}
onclick={onClick}
class={`button button--${size}`}
style:background-color={backgroundColor}
class:button--primary={primary}
class:button--secondary={!primary}
>
{label}
</button>
<style>
.button {
@apply inline-block cursor-pointer rounded-lg border-0 font-semibold leading-none transition-colors hover:opacity-80 hover:shadow-lg;
}
.button--small {
@apply px-2 py-0.5 text-sm;
}
.button--normal {
@apply px-2 py-1 text-base;
}
.button--large {
@apply px-3 py-1.5 text-lg;
}
.button--primary {
@apply bg-red-600 text-white;
}
.button--secondary {
@apply bg-blue-600 text-white;
}
</style>

View file

@ -1,43 +0,0 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Input from './Input.svelte';
const { Story } = defineMeta({
title: 'Input',
component: Input,
tags: ['autodocs'],
argTypes: {
type: {
control: 'select',
options: [
'number',
'button',
'checkbox',
'color',
'date',
'datetime-local',
'email',
'file',
'hidden',
'image',
'month',
'password',
'radio',
'range',
'reset',
'search',
'submit',
'tel',
'text',
'time',
'url',
'week'
]
}
}
});
</script>
<Story name="Text" args={{ label: 'Text', name: 'text', type: 'text' }} />
<Story name="Password" args={{ label: 'Password', name: 'pass', type: 'password' }} />
<Story name="Email" args={{ label: 'Email', name: 'email', type: 'email' }} />

View file

@ -1,36 +0,0 @@
<script lang="ts">
import type { HTMLAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
type InputProps = {
label?: string;
name: string;
type?: HTMLInputTypeAttribute;
} & HTMLAttributes<HTMLDivElement>;
let { label, name, type = 'text', ...props }: InputProps = $props();
</script>
<div {...props} class={twMerge('hestia-input', props.class)}>
{#if label}
<label for={name}>{label}</label>
<div class="line"></div>
{/if}
<input {name} id={name} {type} />
</div>
<style>
.line {
@apply h-8 border border-l-slate-200;
}
.hestia-input {
@apply flex w-fit items-center gap-2 rounded-lg bg-slate-100 p-2 text-slate-700 outline-blue-400 focus-within:outline;
}
.hestia-input > label {
@apply font-display text-lg;
}
.hestia-input > input {
all: unset;
@apply text-lg;
}
</style>

View file

@ -1,31 +0,0 @@
<script lang="ts">
let { title }: { title: string } = $props();
</script>
<header>
<div class="navbar">
<div>
<h2>Hestia</h2>
</div>
<div>
<h1>{title}</h1>
</div>
<div>
<p>Welcome!</p>
</div>
</div>
</header>
<style>
.navbar {
@apply flex items-center justify-between border-b border-slate-200 bg-slate-50 px-6 py-2 font-display drop-shadow;
}
.navbar h1 {
@apply text-2xl;
}
.navbar h2 {
@apply text-xl;
}
</style>

View file

@ -3,9 +3,8 @@
import Navbar from './Navbar.svelte'; import Navbar from './Navbar.svelte';
const { Story } = defineMeta({ const { Story } = defineMeta({
title: 'Navbar', title: 'Navigation/Navbar',
component: Navbar, component: Navbar,
tags: ['autodocs']
}); });
</script> </script>

View file

@ -0,0 +1,12 @@
<script lang="ts">
let { title }: { title: string } = $props();
</script>
<header class="navbar justify-between bg-base-200 px-4">
<h2 class="prose prose-xl">Hestia</h2>
<h1 class="prose prose-2xl">{title}</h1>
<p class="prose prose-lg">Welcome!</p>
piopi commented 2024-12-19 21:05:07 -05:00 (Migrated from github.com)
Review

Make me think that we should probably start using the library i18n, it will help with translation and not hard coding text in the code

Make me think that we should probably start using the library i18n, it will help with translation and not hard coding text in the code
BenjaminPalko commented 2024-12-19 21:19:00 -05:00 (Migrated from github.com)
Review

Yes, it was part of the sveltekit start project but I went with a simpler setup since it was a lot at once. #19

Yes, it was part of the sveltekit start project but I went with a simpler setup since it was a lot at once. #19
</header>
<style>
</style>

View file

@ -0,0 +1,3 @@
import Navbar from './Navbar.svelte';
export default Navbar;

View file

@ -0,0 +1,5 @@
<script lang="ts">
let { active, label, onclick } = $props();
</script>
<input aria-label={label} type="radio" role="tab" class="tab" class:tab-active={active} {onclick} />

View file

@ -0,0 +1,27 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import type { ComponentProps } from 'svelte';
import Tabs from './Tabs.svelte';
const { Story } = defineMeta({
title: 'Navigation/Tabs',
component: Tabs,
tags: ['autodocs'],
argTypes: {
size: {
control: 'select',
options: ['xs', 'sm', 'rg', 'lg'],
},
variant: {
control: 'select',
options: ['none', 'bordered', 'lifted', 'boxed'],
},
},
});
</script>
{#snippet template(args: Partial<ComponentProps<typeof Tabs>>)}
<Tabs tabs={['Tab 1', 'Tab 2']} {...args} />
{/snippet}
<Story name="Default" args={{}} children={template} />

View file

@ -0,0 +1,36 @@
<script lang="ts">
import type { DaisySize } from '$lib/types';
import Tab from './Tab.svelte';
type Props = {
size?: DaisySize;
tabs: string[];
selected?: number;
variant?: 'none' | 'bordered' | 'lifted' | 'boxed';
};
let { size, tabs, selected: value = $bindable(0), variant = 'none' }: Props = $props();
</script>
<div
role="tablist"
class="tabs w-full"
class:tabs-xs={size === 'xs'}
class:tabs-sm={size === 'sm'}
class:tabs-lg={size === 'lg'}
class:tabs-bordered={variant === 'bordered'}
class:tabs-lifted={variant === 'lifted'}
class:tabs-boxed={variant === 'boxed'}
>
{#each tabs as tab, index}
{#key [tab, value]}
<Tab
active={index === value}
label={tab}
onclick={() => {
value = index;
}}
/>
{/key}
{/each}
</div>

View file

@ -0,0 +1,4 @@
import Tabs from './Tabs.svelte';
export default Tabs;
export { default as Tabs } from './Tabs.svelte';

View file

@ -0,0 +1,4 @@
import Navbar from './Navbar';
import Tabs from './Tabs';
export { Navbar, Tabs };

View file

@ -0,0 +1,44 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Button from './Button.svelte';
import { fn } from '@storybook/test';
const { Story } = defineMeta({
title: 'Actions/Button',
component: Button,
args: {
onClick: fn(),
},
argTypes: {
color: {
control: 'select',
options: [
'neutral',
'primary',
'secondary',
'accent',
'ghost',
'link',
'info',
'success',
'warning',
'error',
],
},
outline: {
control: 'boolean',
},
size: {
control: 'select',
options: ['Default', 'xs', 'sm', 'lg'],
defaultValue: 'Default',
},
type: {
control: 'select',
options: ['button', 'reset', 'submit'],
},
},
});
</script>
<Story name="Default" args={{ label: 'Button', color: 'primary' }} />

View file

@ -0,0 +1,57 @@
<script lang="ts">
import type { DaisyColor, DaisySize } from '$lib/types';
import type { HTMLButtonAttributes } from 'svelte/elements';
interface Props {
block?: boolean;
color?: DaisyColor;
glass?: boolean;
label: string;
outline?: boolean;
onClick?: () => void;
responsive?: boolean;
size?: DaisySize;
type?: HTMLButtonAttributes['type'];
wide?: boolean;
}
let {
block = false,
color,
glass = false,
label,
outline = false,
onClick,
responsive = false,
size,
type = 'button',
wide = false,
}: Props = $props();
</script>
<button
{type}
onclick={onClick}
class:btn-outline={outline}
class:btn-block={block}
class:btn-wide={wide}
class:glass
class:btn-xs={size === 'xs'}
class:btn-sm={size === 'sm'}
class:btn-lg={size === 'lg'}
class:btn-neutral={color === 'neutral'}
class:btn-primary={color === 'primary'}
class:btn-secondary={color === 'secondary'}
class:btn-accent={color === 'accent'}
class:btn-ghost={color === 'ghost'}
class:btn-link={color === 'link'}
class:btn-info={color === 'info'}
class:btn-success={color === 'success'}
class:btn-warning={color === 'warning'}
class:btn-error={color === 'error'}
class={`btn ${responsive && 'btn-xs sm:btn-sm md:btn-md lg:btn-lg'}`}
>
{label}
</button>
<style></style>

View file

@ -0,0 +1,3 @@
import Button from './Button.svelte';
export default Button;

View file

@ -3,9 +3,8 @@
import Loader from './Loader.svelte'; import Loader from './Loader.svelte';
const { Story } = defineMeta({ const { Story } = defineMeta({
title: 'Loader', title: 'Feedback/Loader',
component: Loader, component: Loader,
tags: ['autodocs']
}); });
</script> </script>

View file

@ -0,0 +1,3 @@
import Loader from './Loader.svelte';
export default Loader;

View file

@ -0,0 +1,45 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import TextInput from './TextInput.svelte';
const { Story } = defineMeta({
title: 'Data Input/Text Input',
component: TextInput,
argTypes: {
color: {
control: 'select',
options: [
'primary',
'secondary',
'accent',
'ghost',
'link',
'info',
'success',
'warning',
'error',
],
},
bordered: {
control: 'boolean',
},
size: {
control: 'select',
options: ['Default', 'xs', 'sm', 'lg'],
defaultValue: 'Default',
},
type: {
control: 'select',
options: ['email', 'password', 'search', 'tel', 'text', 'url'],
},
},
});
</script>
{#snippet icon()}
<i class="fi fi-rr-user"></i>
{/snippet}
<Story name="Text Label" args={{ color: 'primary', name: 'text', start: 'Text' }} />
<Story name="Icon Start" args={{ color: 'secondary', name: 'text', start: icon }} />
<Story name="Icon End" args={{ color: 'secondary', name: 'text', end: icon }} />

View file

@ -0,0 +1,68 @@
<script lang="ts">
import type { DaisyColor, DaisySize } from '$lib/types';
import type { Snippet } from 'svelte';
import type { HTMLInputTypeAttribute } from 'svelte/elements';
import { fade as fadeTransition } from 'svelte/transition';
type Props = {
bordered?: boolean;
color?: Exclude<DaisyColor, 'neutral'>;
disabled?: boolean;
fade?: boolean;
start?: Snippet | string;
end?: Snippet | string;
name: string;
placeholder?: string;
size?: DaisySize;
type?: Extract<
HTMLInputTypeAttribute,
'email' | 'password' | 'search' | 'tel' | 'text' | 'url'
>;
};
let {
bordered = false,
color,
disabled,
fade,
start,
end,
name,
placeholder,
size,
type = 'text',
}: Props = $props();
</script>
<label
transition:fadeTransition={{ duration: fade ? 200 : 0 }}
class="input flex w-full items-center gap-2"
class:input-bordered={bordered}
class:input-xs={size === 'xs'}
class:input-sm={size === 'sm'}
class:input-lg={size === 'lg'}
class:input-primary={color === 'primary'}
class:input-secondary={color === 'secondary'}
class:input-accent={color === 'accent'}
class:input-ghost={color === 'ghost'}
class:input-link={color === 'link'}
class:input-info={color === 'info'}
class:input-success={color === 'success'}
class:input-warning={color === 'warning'}
class:input-error={color === 'error'}
>
{#if typeof start === 'string'}
{start}
{:else}
{@render start?.()}
{/if}
<input {disabled} {name} {placeholder} {type} class="grow" />
{#if typeof end === 'string'}
{end}
{:else}
{@render end?.()}
{/if}
</label>
<style>
</style>

View file

@ -0,0 +1,3 @@
import TextInput from './TextInput.svelte';
export default TextInput;

View file

@ -1 +0,0 @@
export * from './Navbar.svelte';

View file

@ -8,7 +8,7 @@ export interface Configuration {
export const LoadConfig = (): Configuration => { export const LoadConfig = (): Configuration => {
const { success, data, error } = z const { success, data, error } = z
.object({ .object({
VITE_APP_VERSION: z.string().default('development') VITE_APP_VERSION: z.string().default('development'),
}) })
.safeParse(import.meta.env); .safeParse(import.meta.env);
@ -17,7 +17,7 @@ export const LoadConfig = (): Configuration => {
} }
return { return {
app_version: data!.VITE_APP_VERSION app_version: data!.VITE_APP_VERSION,
}; };
}; };

View file

@ -19,5 +19,5 @@ export const yogaLogger: YogaLogger = {
error(...args) { error(...args) {
// @ts-expect-error types dont match // @ts-expect-error types dont match
logger.error(...args); logger.error(...args);
} },
}; };

View file

@ -7,14 +7,14 @@ const adapter = new PrismaAdapter(prisma.session, prisma.user);
export const auth = new Lucia(adapter, { export const auth = new Lucia(adapter, {
sessionCookie: { sessionCookie: {
attributes: { attributes: {
secure: process.env.NODE_ENV === 'production' secure: process.env.NODE_ENV === 'production',
} },
}, },
getUserAttributes: (attributes) => { getUserAttributes: (attributes) => {
return { return {
email: attributes.email email: attributes.email,
}; };
} },
}); });
declare module 'lucia' { declare module 'lucia' {

View file

@ -23,6 +23,6 @@ export const builder = new SchemaBuilder<PothosType>({
// use where clause from prismaRelatedConnection for totalCount (defaults to true) // use where clause from prismaRelatedConnection for totalCount (defaults to true)
filterConnectionTotalCount: true, filterConnectionTotalCount: true,
// warn when not using a query parameter correctly // warn when not using a query parameter correctly
onUnusedQuery: process.env.NODE_ENV === 'production' ? null : 'warn' onUnusedQuery: process.env.NODE_ENV === 'production' ? null : 'warn',
} },
}); });

View file

@ -10,5 +10,5 @@ export const DateScalar = builder.scalarType('Date', {
throw new Error('Cyka blyat'); throw new Error('Cyka blyat');
} }
return new Date(date); return new Date(date);
} },
}); });

View file

@ -5,7 +5,7 @@ builder.queryType({});
builder.queryField('version', (t) => builder.queryField('version', (t) =>
t.string({ t.string({
description: 'Application version', description: 'Application version',
resolve: (parent, args, context) => context.config.app_version resolve: (parent, args, context) => context.config.app_version,
}) })
); );

View file

@ -9,39 +9,39 @@ export const Post = builder.prismaObject('Post', {
published: t.exposeBoolean('published'), published: t.exposeBoolean('published'),
author: t.relation('author'), author: t.relation('author'),
createdAt: t.expose('createdAt', { createdAt: t.expose('createdAt', {
type: 'Date' type: 'Date',
}), }),
updatedAt: t.expose('updatedAt', { updatedAt: t.expose('updatedAt', {
type: 'Date' type: 'Date',
}) }),
}) }),
}); });
const CreatePost = builder.inputType('CreatePost', { const CreatePost = builder.inputType('CreatePost', {
fields: (t) => ({ fields: (t) => ({
title: t.string({ title: t.string({
required: true required: true,
}), }),
content: t.string({ content: t.string({
required: true required: true,
}), }),
published: t.boolean(), published: t.boolean(),
authorId: t.id({ authorId: t.id({
required: true required: true,
}) }),
}) }),
}); });
const UpdatePost = builder.inputType('UpdatePost', { const UpdatePost = builder.inputType('UpdatePost', {
fields: (t) => ({ fields: (t) => ({
id: t.id({ id: t.id({
required: true required: true,
}), }),
title: t.string(), title: t.string(),
content: t.string(), content: t.string(),
published: t.boolean(), published: t.boolean(),
authorId: t.id() authorId: t.id(),
}) }),
}); });
builder.queryFields((t) => ({ builder.queryFields((t) => ({
@ -49,19 +49,19 @@ builder.queryFields((t) => ({
type: [Post], type: [Post],
resolve: async () => { resolve: async () => {
return await prisma.post.findMany(); return await prisma.post.findMany();
} },
}) }),
})); }));
builder.mutationFields((t) => ({ builder.mutationFields((t) => ({
createPost: t.field({ createPost: t.field({
type: Post, type: Post,
args: { args: {
input: t.arg({ required: true, type: CreatePost }) input: t.arg({ required: true, type: CreatePost }),
}, },
resolve: async (parent, args) => { resolve: async (parent, args) => {
const author = await prisma.user.findUnique({ const author = await prisma.user.findUnique({
where: { id: Number(args.input.authorId) } where: { id: Number(args.input.authorId) },
}); });
if (!author) { if (!author) {
throw new Error('Author does not exist!'); throw new Error('Author does not exist!');
@ -73,23 +73,23 @@ builder.mutationFields((t) => ({
published: args.input.published, published: args.input.published,
author: { author: {
connect: { connect: {
id: author.id id: author.id,
} },
} },
} },
}); });
return post; return post;
} },
}), }),
updatePost: t.field({ updatePost: t.field({
type: Post, type: Post,
args: { args: {
input: t.arg({ required: true, type: UpdatePost }) input: t.arg({ required: true, type: UpdatePost }),
}, },
resolve: async (parent, args) => { resolve: async (parent, args) => {
const post = await prisma.post.update({ const post = await prisma.post.update({
where: { where: {
id: Number(args.input.id) id: Number(args.input.id),
}, },
data: { data: {
title: args.input.title ?? undefined, title: args.input.title ?? undefined,
@ -98,13 +98,13 @@ builder.mutationFields((t) => ({
...(args.input.authorId && { ...(args.input.authorId && {
author: { author: {
connect: { connect: {
id: Number(args.input.authorId) id: Number(args.input.authorId),
} },
} },
}) }),
} },
}); });
return post; return post;
} },
}) }),
})); }));

View file

@ -8,33 +8,33 @@ export const User = builder.prismaObject('User', {
name: t.exposeString('name'), name: t.exposeString('name'),
posts: t.relation('posts'), posts: t.relation('posts'),
createdAt: t.expose('createdAt', { createdAt: t.expose('createdAt', {
type: 'Date' type: 'Date',
}), }),
updatedAt: t.expose('updatedAt', { updatedAt: t.expose('updatedAt', {
type: 'Date' type: 'Date',
}) }),
}) }),
}); });
const CreateUser = builder.inputType('CreateUser', { const CreateUser = builder.inputType('CreateUser', {
fields: (t) => ({ fields: (t) => ({
email: t.string({ email: t.string({
required: true required: true,
}), }),
name: t.string({ name: t.string({
required: true required: true,
}) }),
}) }),
}); });
const UpdateUser = builder.inputType('UpdateUser', { const UpdateUser = builder.inputType('UpdateUser', {
fields: (t) => ({ fields: (t) => ({
id: t.id({ id: t.id({
required: true required: true,
}), }),
email: t.string(), email: t.string(),
name: t.string() name: t.string(),
}) }),
}); });
builder.queryFields((t) => ({ builder.queryFields((t) => ({
@ -42,42 +42,42 @@ builder.queryFields((t) => ({
type: [User], type: [User],
resolve: async () => { resolve: async () => {
return await prisma.user.findMany(); return await prisma.user.findMany();
} },
}) }),
})); }));
builder.mutationFields((t) => ({ builder.mutationFields((t) => ({
createUser: t.field({ createUser: t.field({
type: User, type: User,
args: { args: {
input: t.arg({ required: true, type: CreateUser }) input: t.arg({ required: true, type: CreateUser }),
}, },
resolve: async (parent, args) => { resolve: async (parent, args) => {
const post = await prisma.user.create({ const post = await prisma.user.create({
data: { data: {
email: args.input.email, email: args.input.email,
name: args.input.name name: args.input.name,
} },
}); });
return post; return post;
} },
}), }),
updateUser: t.field({ updateUser: t.field({
type: User, type: User,
args: { args: {
input: t.arg({ required: true, type: UpdateUser }) input: t.arg({ required: true, type: UpdateUser }),
}, },
resolve: async (parent, args) => { resolve: async (parent, args) => {
const post = await prisma.user.update({ const post = await prisma.user.update({
where: { where: {
id: Number(args.input.id) id: Number(args.input.id),
}, },
data: { data: {
email: args.input.email, email: args.input.email,
name: args.input.name ?? undefined name: args.input.name ?? undefined,
} },
}); });
return post; return post;
} },
}) }),
})); }));

View file

@ -3,5 +3,5 @@ import type { YogaInitialContext } from 'graphql-yoga';
export const Context = (initialContext: YogaInitialContext) => ({ export const Context = (initialContext: YogaInitialContext) => ({
...initialContext, ...initialContext,
config: Config config: Config,
}); });

View file

@ -10,5 +10,5 @@ export const Yoga = createYoga<RequestEvent>({
graphqlEndpoint: '/api/graphql', graphqlEndpoint: '/api/graphql',
// Let Yoga use sveltekit's Response object // Let Yoga use sveltekit's Response object
fetchAPI: { Response }, fetchAPI: { Response },
logging: yogaLogger logging: yogaLogger,
}); });

View file

@ -0,0 +1,12 @@
export type DaisyColor =
| 'default'
| 'neutral'
| 'primary'
| 'secondary'
| 'accent'
| 'ghost'
| 'link'
| 'info'
| 'success'
| 'warning'
| 'error';

View file

@ -0,0 +1 @@
export type DaisySize = 'xs' | 'sm' | 'lg';

2
src/lib/types/index.ts Normal file
View file

@ -0,0 +1,2 @@
export * from './daisy-colors';
export * from './daisy-sizes';

View file

@ -9,6 +9,6 @@
<style> <style>
.layout { .layout {
@apply h-screen w-screen animate-fade bg-slate-100; @apply h-screen w-screen bg-base-100;
} }
</style> </style>

View file

@ -1,17 +1,17 @@
import { prisma } from '$lib/server/prisma'; import { prisma } from '$lib/server/prisma';
import { redirect } from '@sveltejs/kit';
export async function load(event) { export async function load(event) {
const userId = event.cookies.get('user'); const sessionId = event.cookies.get('auth_session');
if (!userId) { if (!sessionId) {
return { redirect(303, '/login');
authenticated: false
};
} }
const user = await prisma.user.findUnique({ const user = await prisma.session.findUnique({
where: { where: {
id: userId id: sessionId,
} },
}); });
return { if (!user) {
authenticated: !!user redirect(401, '/login');
}; }
return {};
} }

View file

@ -1,19 +1,17 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Loader from '$lib/components/Loader.svelte'; import Loader from '$lib/components/common/Loader';
import { fade } from 'svelte/transition';
let { data } = $props();
$effect(() => { $effect(() => {
const id = setTimeout(() => (data.authenticated ? goto('/app') : goto('/login')), 1500); const id = setTimeout(() => goto('/app'), 1500);
return () => { return () => {
clearTimeout(id); clearTimeout(id);
}; };
}); });
</script> </script>
<div class="site-loader"> <div class="site-loader" transition:fade>
<h1>Hestia</h1>
<Loader /> <Loader />
</div> </div>

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Navbar from '$lib/components/Navbar.svelte'; import { Navbar } from '$lib/components/Navigation';
let { children } = $props(); let { children } = $props();
</script> </script>

View file

@ -12,8 +12,8 @@ export const actions = {
} }
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
email: form.get('email') as string email: form.get('email') as string,
} },
}); });
if (!user) { if (!user) {
logger.error('User not found! ${user}'); logger.error('User not found! ${user}');
@ -31,7 +31,7 @@ export const actions = {
const sessionCookie = auth.createSessionCookie(session.id); const sessionCookie = auth.createSessionCookie(session.id);
event.cookies.set(sessionCookie.name, sessionCookie.value, { event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: '/', path: '/',
maxAge: 120 maxAge: 120,
}); });
redirect(302, '/'); redirect(302, '/');
}, },
@ -47,8 +47,8 @@ export const actions = {
data: { data: {
email: form.get('email') as string, email: form.get('email') as string,
name: form.get('name') as string, name: form.get('name') as string,
password: hashedPassword password: hashedPassword,
} },
}); });
const session = await auth.createSession(user.id.toString(), {}); const session = await auth.createSession(user.id.toString(), {});
const sessionCookie = auth.createSessionCookie(session.id); const sessionCookie = auth.createSessionCookie(session.id);
@ -57,8 +57,8 @@ export const actions = {
} }
event.cookies.set(sessionCookie.name, sessionCookie.value, { event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: '/', path: '/',
maxAge: 120 maxAge: 120,
}); });
redirect(302, '/'); redirect(302, '/');
} },
} satisfies Actions; } satisfies Actions;

View file

@ -1,38 +1,50 @@
<script lang="ts"> <script lang="ts">
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/common/Button';
import Input from '$lib/components/Input.svelte'; import TextInput from '$lib/components/common/TextInput';
import { fade, scale } from 'svelte/transition'; import Tabs from '$lib/components/Navigation/Tabs';
import { fade } from 'svelte/transition';
let mode: 'register' | 'login' = $state('login'); let tab: 0 | 1 = $state(0);
let action = $derived(mode === 'login' ? '?/login' : '?/register');
function onViewToggle() {
mode = mode === 'login' ? 'register' : 'login';
}
</script> </script>
<div class="page"> {#snippet userIcon()}
<h1 class="underline">Hestia</h1> <i class="fi fi-br-envelope"></i>
<div class="login"> {/snippet}
<form method="POST" {action} transition:scale>
<h2 transition:fade>{mode === 'login' ? 'Login' : 'Register'}</h2> {#snippet passwordIcon()}
{#if mode === 'register'} <i class="fi fi-br-key"></i>
<div transition:fade> {/snippet}
<Input label="Name" name="name" />
</div> {#snippet nameIcon()}
{/if} <i class="fi fi-rr-user"></i>
<Input label="Email" name="email" type="email" /> {/snippet}
<Input label="Password" name="password" type="password" />
<div class="flex gap-2"> {#snippet form(variant: 'login' | 'register')}
<Button <form method="POST" action={`?/${variant}`}>
onClick={onViewToggle} <div class="card-body gap-4">
label={mode === 'login' ? 'Register' : 'Login'} <TextInput start={userIcon} placeholder="Email" name="email" type="email" />
size="large" <TextInput
primary start={passwordIcon}
placeholder="Password"
name="password"
type="password"
/> />
<Button type="submit" label="Submit" size="large" /> {#if variant === 'register'}
<TextInput start={nameIcon} placeholder="Name" name="name" fade />
{/if}
</div>
<div class="card-actions px-4">
<Button block type="submit" label="Submit" outline />
</div> </div>
</form> </form>
{/snippet}
<div class="page" transition:fade>
<div class="card bg-base-200 py-4 shadow-xl">
<div class="card-title">
<Tabs variant="bordered" bind:selected={tab} tabs={['Login', 'Register']} />
</div>
{@render form(tab === 0 ? 'login' : 'register')}
</div> </div>
</div> </div>
@ -40,10 +52,4 @@
.page { .page {
@apply flex flex-col items-center justify-around gap-24 py-[10%]; @apply flex flex-col items-center justify-around gap-24 py-[10%];
} }
.login {
@apply w-fit max-w-lg animate-fade rounded-lg bg-white p-8;
}
.login > form {
@apply flex w-full flex-col items-center gap-8 rounded-lg;
}
</style> </style>

View file

@ -11,8 +11,8 @@ const config = {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter. // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters. // See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter() adapter: adapter(),
} },
}; };
export default config; export default config;

View file

@ -1,4 +1,5 @@
import typography from '@tailwindcss/typography'; import typography from '@tailwindcss/typography';
import daisyui from 'daisyui';
import type { Config } from 'tailwindcss'; import type { Config } from 'tailwindcss';
export default { export default {
@ -7,19 +8,22 @@ export default {
theme: { theme: {
extend: { extend: {
fontFamily: { fontFamily: {
display: ['Baskervville SC'] display: ['Baskervville SC'],
}, },
animation: { animation: {
fade: 'fadeIn .5s ease-in-out' fade: 'fadeIn .5s ease-in-out',
}, },
keyframes: { keyframes: {
fadeIn: { fadeIn: {
from: { opacity: '0' }, from: { opacity: '0' },
to: { opacity: '1' } to: { opacity: '1' },
} },
} },
} },
}, },
plugins: [typography] plugins: [typography, daisyui],
daisyui: {
logs: false,
},
} satisfies Config; } satisfies Config;

View file

@ -5,6 +5,6 @@ export default defineConfig({
plugins: [sveltekit()], plugins: [sveltekit()],
test: { test: {
include: ['src/**/*.{test,spec}.{js,ts}'] include: ['src/**/*.{test,spec}.{js,ts}'],
} },
}); });