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.ts.timestamp-*
*storybook.log
*/dev.db
*/dev.db-journal

View file

@ -1,8 +1,14 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"trailingComma": "es5",
"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"],
"overrides": [
{

View file

@ -2,15 +2,16 @@
const config = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|ts|svelte)'],
addons: [
'@storybook/addon-svelte-csf',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'storybook-dark-mode'
'@storybook/addon-styling-webpack',
'@storybook/addon-svelte-csf',
'@storybook/addon-themes',
],
framework: {
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 } */
const preview = {
tags: ['autodocs'],
parameters: {
controls: {
matchers: {
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
If you're seeing this, you've probably already done this step. Congrats!
## Setup
```bash
# create a new project in the current directory
bunx sv create
# install dependencies
bun install
# create a new project in my-app
bunx sv create my-app
# set up local database
bun prisma:dev
```
## 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
bun run dev
bun dev
# 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
To create a production version of your app:
```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`.
> 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: {
globals: {
...globals.browser,
...globals.node
}
}
...globals.node,
},
},
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
parser: ts.parser,
},
},
}
);

View file

@ -15,7 +15,12 @@
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"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": {
"@chromatic-com/storybook": "^3.2.2",
@ -23,7 +28,9 @@
"@playwright/test": "^1.45.3",
"@storybook/addon-essentials": "^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-themes": "^8.4.7",
"@storybook/blocks": "^8.4.7",
"@storybook/svelte": "^8.4.7",
"@storybook/sveltekit": "^8.4.7",
@ -33,6 +40,7 @@
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@types/bun": "^1.1.14",
"autoprefixer": "^10.4.20",
"daisyui": "^4.12.22",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
@ -42,7 +50,6 @@
"prettier-plugin-tailwindcss": "^0.6.5",
"prisma": "^6.0.1",
"storybook": "^8.4.7",
"storybook-dark-mode": "^4.0.2",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^3.4.9",
@ -52,6 +59,7 @@
"vitest": "^2.0.4"
},
"dependencies": {
"@flaticon/flaticon-uicons": "^3.3.1",
"@lucia-auth/adapter-prisma": "^4.0.1",
"@pothos/core": "^4.3.0",
"@pothos/plugin-prisma": "^4.4.0",

View file

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

View file

@ -1,6 +1,6 @@
export default {
plugins: {
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

@ -15,34 +15,38 @@ datasource db {
}
model User {
id String @id @default(uuid())
email String? @unique
name String
password String
posts Post[]
sessions Session[]
id String @id @default(uuid())
email String? @unique
name String
password String
posts Post[]
sessions Session[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}
model Session {
id String @id @default(uuid())
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
userId String
user User @relation(references: [id], fields: [userId])
id String @id @default(uuid())
expiresAt DateTime
user User @relation(references: [id], fields: [userId])
userId String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}
model Post {
id String @id @default(uuid())
id String @id @default(uuid())
title String
content String
published Boolean? @default(false)
author User @relation(references: [id], fields: [authorId])
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}
}

View file

@ -1,19 +1,8 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import '@flaticon/flaticon-uicons/css/all/all';
: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
declare global {
namespace App {
// interface Error {}
interface Locals {
user: import("lucia").User | null;
user: import('lucia').User | 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 {}

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,10 +3,9 @@
import Navbar from './Navbar.svelte';
const { Story } = defineMeta({
title: 'Navbar',
title: 'Navigation/Navbar',
component: Navbar,
tags: ['autodocs']
});
</script>
<Story name="Default" args={{ title: 'Storybook' }} />
<Story name="Default" args={{ title: 'Storybook' }} />

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,10 +3,9 @@
import Loader from './Loader.svelte';
const { Story } = defineMeta({
title: 'Loader',
title: 'Feedback/Loader',
component: Loader,
tags: ['autodocs']
});
</script>
<Story name="Default" args={{}} />
<Story name="Default" args={{}} />

View file

@ -48,4 +48,4 @@
transform: rotate(-360deg);
}
}
</style>
</style>

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

@ -1,2 +1,2 @@
// place files you want to import through the `$lib` alias in this folder.
export * from './components';
export * from './components';

View file

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

View file

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

View file

@ -7,14 +7,14 @@ const adapter = new PrismaAdapter(prisma.session, prisma.user);
export const auth = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: process.env.NODE_ENV === 'production'
}
secure: process.env.NODE_ENV === 'production',
},
},
getUserAttributes: (attributes) => {
return {
email: attributes.email
email: attributes.email,
};
}
},
});
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)
filterConnectionTotalCount: true,
// 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

@ -1 +1 @@
export * from './schema';
export * from './schema';

View file

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

View file

@ -5,4 +5,4 @@ export type Scalars = {
Input: Date;
Output: Date;
};
};
};

View file

@ -5,7 +5,7 @@ builder.queryType({});
builder.queryField('version', (t) =>
t.string({
description: 'Application version',
resolve: (parent, args, context) => context.config.app_version
resolve: (parent, args, context) => context.config.app_version,
})
);
@ -15,4 +15,4 @@ import './Scalars';
import './posts';
import './users';
export const Schema = builder.toSchema();
export const Schema = builder.toSchema();

View file

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

View file

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

View file

@ -1,3 +1,3 @@
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();
export const prisma = new PrismaClient();

View file

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

View file

@ -1,2 +1,2 @@
export * from './context';
export * from './server';
export * from './server';

View file

@ -10,5 +10,5 @@ export const Yoga = createYoga<RequestEvent>({
graphqlEndpoint: '/api/graphql',
// Let Yoga use sveltekit's Response object
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>
.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 { redirect } from '@sveltejs/kit';
export async function load(event) {
const userId = event.cookies.get('user');
if (!userId) {
return {
authenticated: false
};
const sessionId = event.cookies.get('auth_session');
if (!sessionId) {
redirect(303, '/login');
}
const user = await prisma.user.findUnique({
const user = await prisma.session.findUnique({
where: {
id: userId
}
id: sessionId,
},
});
return {
authenticated: !!user
};
if (!user) {
redirect(401, '/login');
}
return {};
}

View file

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

View file

@ -1,3 +1,3 @@
import { Yoga } from '$lib/server/yoga';
export { Yoga as GET, Yoga as POST };
export { Yoga as GET, Yoga as POST };

View file

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

View file

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

View file

@ -1,38 +1,50 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import Input from '$lib/components/Input.svelte';
import { fade, scale } from 'svelte/transition';
import Button from '$lib/components/common/Button';
import TextInput from '$lib/components/common/TextInput';
import Tabs from '$lib/components/Navigation/Tabs';
import { fade } from 'svelte/transition';
let mode: 'register' | 'login' = $state('login');
let action = $derived(mode === 'login' ? '?/login' : '?/register');
function onViewToggle() {
mode = mode === 'login' ? 'register' : 'login';
}
let tab: 0 | 1 = $state(0);
</script>
<div class="page">
<h1 class="underline">Hestia</h1>
<div class="login">
<form method="POST" {action} transition:scale>
<h2 transition:fade>{mode === 'login' ? 'Login' : 'Register'}</h2>
{#if mode === 'register'}
<div transition:fade>
<Input label="Name" name="name" />
</div>
{#snippet userIcon()}
<i class="fi fi-br-envelope"></i>
{/snippet}
{#snippet passwordIcon()}
<i class="fi fi-br-key"></i>
{/snippet}
{#snippet nameIcon()}
<i class="fi fi-rr-user"></i>
{/snippet}
{#snippet form(variant: 'login' | 'register')}
<form method="POST" action={`?/${variant}`}>
<div class="card-body gap-4">
<TextInput start={userIcon} placeholder="Email" name="email" type="email" />
<TextInput
start={passwordIcon}
placeholder="Password"
name="password"
type="password"
/>
{#if variant === 'register'}
<TextInput start={nameIcon} placeholder="Name" name="name" fade />
{/if}
<Input label="Email" name="email" type="email" />
<Input label="Password" name="password" type="password" />
<div class="flex gap-2">
<Button
onClick={onViewToggle}
label={mode === 'login' ? 'Register' : 'Login'}
size="large"
primary
/>
<Button type="submit" label="Submit" size="large" />
</div>
</form>
</div>
<div class="card-actions px-4">
<Button block type="submit" label="Submit" outline />
</div>
</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>
@ -40,10 +52,4 @@
.page {
@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.
// 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.
adapter: adapter()
}
adapter: adapter(),
},
};
export default config;

View file

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

View file

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