Daisy UI (#14)

* add dependency

* rename Input to TextInput and use daisy

* base styling

* storybook setup with tailwind and theme changer

* daisy buttons

* add flaticons

* text input to daisy

* Navbar to daisy

* login using daisy

* autodocs is... auto

* refactor Tabs to separate components

* move TextInput

* move button

* move navbar

* remove index

* move container

* move loader

* move tabs to navigation

* organize storybook hierarchy

* use card

* remove storybook dark mode

* README

* ignore db file

* ignore db

* prisma scripts

* format

* blyat

* fix redirect
This commit is contained in:
Baobeld 2024-12-19 21:20:21 -05:00 committed by GitHub
parent 992eb07f5c
commit 6ddaa69f69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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,
"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;
} }
// 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>
</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}'],
} },
}); });