Merge pull request #10 from zoriya/my_front

Added tailwind svelte component framework
This commit is contained in:
2024-05-05 20:09:29 +02:00
committed by GitHub
29 changed files with 4708 additions and 2580 deletions

View File

@@ -31,5 +31,19 @@ services:
timeout: 5s
retries: 5
web:
build:
context: ./web
dockerfile: Dockerfile.dev
restart: on-failure
volumes:
- ./web:/app
ports:
- 5173:5173
environment:
- API_URL=http://api:1597
depends_on:
- api
volumes:
db:

12
web/Dockerfile.dev Normal file
View File

@@ -0,0 +1,12 @@
FROM node:22-alpine
WORKDIR /app
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
CMD ["npm", "run", "dev", "--", "--host"]

6239
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,33 @@
{
"name": "web",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@iconify-json/mingcute": "^1.1.17",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"svelte-time": "^0.9.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"unplugin-icons": "^0.19.0",
"vite": "^5.0.3"
},
"type": "module"
"name": "web",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@iconify-json/mingcute": "^1.1.17",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"autoprefixer": "^10.4.16",
"flowbite": "^2.3.0",
"flowbite-svelte": "^0.46.1",
"flowbite-svelte-icons": "1.6",
"postcss": "^8.4.32",
"postcss-load-config": "^5.0.2",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"svelte-time": "^0.9.0",
"tailwindcss": "^3.3.6",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"unplugin-icons": "^0.19.0",
"vite": "^5.0.3"
},
"type": "module"
}

13
web/postcss.config.cjs Normal file
View File

@@ -0,0 +1,13 @@
const tailwindcss = require("tailwindcss");
const autoprefixer = require("autoprefixer");
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer,
],
};
module.exports = config;

View File

@@ -16,6 +16,7 @@
</body>
<style>
body {
padding: 40px;
font-family: 'Hanken Grotesk', sans-serif;
font-optical-sizing: auto;
font-weight: 400;

4
web/src/app.pcss Normal file
View File

@@ -0,0 +1,4 @@
/* Write your global styles here, in PostCSS syntax */
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,67 @@
<script context="module" lang="ts">
import { writable, type Writable } from 'svelte/store';
export interface TabCtxType {
activeClasses: string;
inactiveClasses: string;
selected: Writable<HTMLElement>;
}
</script>
<script lang="ts">
// import { twMerge } from 'tailwind-merge';
import { setContext } from 'svelte';
export let tabStyle: 'full' | 'pill' | 'underline' | 'none' = 'none';
export let defaultClass: string = 'flex flex-wrap space-x-2 rtl:space-x-reverse';
export let contentClass: string = 'p-4 bg-gray-50 rounded-lg dark:bg-gray-800 mt-4';
export let divider: boolean = true;
export let activeClasses: string = 'p-4 text-primary-600 bg-gray-100 rounded-t-lg dark:bg-gray-800 dark:text-primary-500';
export let inactiveClasses: string = 'p-4 text-gray-500 rounded-t-lg hover:text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-300';
// styles
const styledActiveClasses = {
full: 'p-4 w-full group-first:rounded-s-lg group-last:rounded-e-lg text-gray-900 bg-gray-100 focus:ring-4 focus:ring-primary-300 focus:outline-none dark:bg-gray-700 dark:text-white',
pill: 'py-3 px-4 text-white bg-primary-600 rounded-lg',
underline: 'p-4 text-primary-600 border-b-2 border-primary-600 dark:text-primary-500 dark:border-primary-500',
none: ''
};
const styledInactiveClasses = {
full: 'p-4 w-full group-first:rounded-s-lg group-last:rounded-e-lg text-gray-500 dark:text-gray-400 bg-white hover:text-gray-700 hover:bg-gray-50 focus:ring-4 focus:ring-primary-300 focus:outline-none dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700',
pill: 'py-3 px-4 text-gray-500 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white',
underline: 'p-4 border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 text-gray-500 dark:text-gray-400',
none: ''
};
const ctx: TabCtxType = {
activeClasses: styledActiveClasses[tabStyle] || activeClasses,
inactiveClasses: styledInactiveClasses[tabStyle] || inactiveClasses,
selected: writable<HTMLElement>()
};
$: divider = ['full', 'pill'].includes(tabStyle) ? false : divider;
setContext('ctx', ctx);
function init(node: HTMLElement) {
const destroy = ctx.selected.subscribe((x: HTMLElement) => {
if (x) node.replaceChildren(x);
});
return { destroy };
}
// $: ulClass = twMerge(defaultClass, tabStyle === 'underline' && '-mb-px', $$props.class);
</script>
<ul class={ulClass}>
<slot {tabStyle} />
</ul>
{#if divider}
<slot name="divider">
<div class="h-px bg-gray-200 dark:bg-gray-700" />
</slot>
{/if}

View File

@@ -0,0 +1,51 @@
<script lang="ts">
import type { Feed } from "$lib/types";
import TagList from "$lib/posts/tag-list.svelte";
import User from "virtual:icons/mingcute/user-5-line";
export let feed: Feed;
let viewTags = false;
let hover = false;
</script>
<article>
<div>
<img src={feed.faviconUrl} alt={feed.name} />
<h3>{feed.name}</h3>
</div>
<div>
<span>{feed.link}</span>
</div>
{#if feed.submitter}
<div>
Added by
<User />
<span title={feed.submitter.email}>{feed.submitter.name}</span>
</div>
{/if}
{#if viewTags}
<TagList tags={feed.tags} />
{:else}
<button on:click={() => (viewTags = true)}>View tags</button>
{/if}
</article>
<style>
div {
display: flex;
align-items: center;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
gap: 6px;
}
article {
border-radius: 10px;
padding: 0.7rem;
}
img {
width: 40px;
height: 40px;
margin-right: 0.5rem;
}
</style>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import Card from './card.svelte';
import type { Feed } from '$lib/types';
export let feeds: Feed[] = [];
</script>
<section>
<h1>All Feeds</h1>
<h1>Feeds</h1>
<ul>
{#each feeds as feed}
<li>
<Card {feed} />
</li>
{/each}
</ul>
</section>
<style>
ul {
list-style: none;
padding: 0;
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>

View File

@@ -1,46 +1,47 @@
<script lang="ts">
import type { Post } from "$lib/types";
import Time, { dayjs } from "svelte-time";
import TagList from "./tag-list.svelte";
import QuillPenLine from "virtual:icons/mingcute/quill-pen-line";
export let post: Post;
</script>
<article>
<div class="origin">
<img src={post.feed.faviconUrl} alt={post.feed.title} />
<span>{post.feed.title}</span> |
{#if post.author}
<div class="author">
<QuillPenLine />
<span>{post.author}</span>
</div>
|
{/if}
<Time
timestamp={post.date}
relative={true}
title={dayjs(post.date).locale("en").toString()}
/>
</div>
<content>
<h2>{post.title}</h2>
<p>{post.content}</p>
</content>
<footer>
<a
href={post.link}
target="_blank"
on:click={() => {
console.log("clicked");
}}
>
<article>
<div class="origin">
<img src={post.feed.faviconUrl} alt={post.feed.name} />
<span>{post.feed.name}</span> |
{#if post.authors?.length}
{#each post.authors as author}
<div class="author">
<QuillPenLine />
<span>{author}</span>
</div>
|
{/each}
{/if}
<Time
timestamp={post.date}
relative={true}
title={dayjs(post.date).locale("en").toString()}
/>
</div>
<content>
<h2>{post.title}</h2>
<p>{post.content}</p>
</content>
<!-- <footer>
<TagList tags={post.feed.tags} />
</footer>
</article>
</footer> -->
</article>
</a>
<style>
article {
border-radius: 10px;
/* box-shadow: 0px 0px 12px 9px rgba(0, 0, 0, 0.28); */
border: 1px solid #e0e0e0;
padding: 0.7rem;
background-color: rgb(220, 220, 220);
}
.origin {
display: flex;
align-items: center;
@@ -58,6 +59,7 @@
h2 {
margin: 0.5rem;
font-size: 1.3rem;
font-weight: 600;
}
p {

View File

@@ -4,13 +4,14 @@
export let posts: Post[] = [];
</script>
<h1>Posts</h1>
<ul>
{#each posts as post}
{#each posts as post, index}
<li>
<Card {post} />
</li>
{#if index !== posts.length - 1}
<hr />
{/if}
{/each}
</ul>
@@ -19,8 +20,7 @@
list-style: none;
padding: 0;
display: flex;
flex-direction: column;
gap: 1rem;
flex-direction: column;
}
</style>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
export let tags: string[] = [];
export let nbNonExpandedTags = tags.length;
const nbNonExpandedTags = 5;
let isExpanded = false;
$: tagsToDisplay = isExpanded ? tags : tags.slice(0, nbNonExpandedTags);
</script>

View File

@@ -0,0 +1,109 @@
<script lang="ts">
import { twMerge } from "tailwind-merge";
import { getContext } from "svelte";
import Time, { dayjs } from "svelte-time";
import { P } from "flowbite-svelte";
import QuillPenLine from "virtual:icons/mingcute/quill-pen-line";
import type { Post } from "$lib/types";
export let title: string = "";
export let date: string = "";
export let post: Post;
export let svgClass: string =
"w-3 h-3 text-primary-600 dark:text-primary-400";
let order: "default" | "vertical" | "horizontal" | "activity" | "group" =
"default";
order = getContext("order");
const liClasses = {
default: "mb-10 ms-4",
vertical: "mb-10 ms-6",
horizontal: "relative mb-6 sm:mb-0",
activity: "mb-10 ms-6",
group: "",
};
const divClasses = {
default:
"absolute w-3 h-3 bg-gray-200 rounded-full mt-1.5 -start-1.5 border border-white dark:border-gray-900 dark:bg-gray-700",
vertical:
"flex absolute -start-3 justify-center items-center w-6 h-6 bg-primary-200 rounded-full ring-8 ring-white dark:ring-gray-900 dark:bg-primary-900",
horizontal: "flex items-center",
activity:
"flex absolute -start-3 justify-center items-center w-6 h-6 bg-primary-200 rounded-full ring-8 ring-white dark:ring-gray-900 dark:bg-primary-900",
group:
"p-5 mb-4 bg-gray-50 rounded-lg border border-gray-100 dark:bg-gray-800 dark:border-gray-700",
};
const timeClasses = {
default:
"mb-1 text-sm font-normal leading-none text-gray-400 dark:text-gray-500",
vertical:
"block mb-2 text-sm font-normal leading-none text-gray-400 dark:text-gray-500",
horizontal:
"block mb-2 text-sm font-normal leading-none text-gray-400 dark:text-gray-500",
activity: "mb-1 text-xs font-normal text-gray-400 sm:order-last sm:mb-0",
group: "text-lg font-semibold text-gray-900 dark:text-white",
};
let liCls: string = twMerge(liClasses[order], $$props.classLi);
let divCls: string = twMerge(divClasses[order], $$props.classDiv);
let timeCls: string = twMerge(timeClasses[order], $$props.classTime);
const h3Cls = twMerge(
order === "vertical"
? "flex items-center mb-1 text-lg font-semibold text-gray-900 dark:text-white"
: "text-lg font-semibold text-gray-900 dark:text-white",
$$props.classH3,
);
</script>
<li class={liCls}>
<div class={divCls} />
{#if order !== "default"}
<slot name="icon">
<svg
aria-hidden="true"
class={svgClass}
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
clip-rule="evenodd"
/>
</svg>
</slot>
{:else if date}
<time class={timeCls}>{date}</time>
{/if}
{#if title}
<a href={post.link} target="_blank">
<h3 class={h3Cls}>
{title}
</h3>
</a>
{/if}
{#if order !== "default"}
<div class="font-light text-sm text-slate-500 flex gap-2">
<span>{post.feed.name}</span> |
{#if post.authors?.length}
{#each post.authors as author}
<div class="flex gap-1 items-center">
<QuillPenLine />
<span>{author}</span>
</div> |
{/each}
{/if}
<Time
timestamp={post.date}
relative={true}
title={dayjs(post.date).locale("en").toString()}
/>
</div>
{/if}
<slot />
</li>

36
web/src/lib/server/api.ts Normal file
View File

@@ -0,0 +1,36 @@
import { env } from '$env/dynamic/private';
import type { Post } from '$lib/types';
export async function login(email: string, password: string) {
const r = await fetch(env.API_URL + '/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
const j = await r.json();
return j.token as string;
}
export async function register(email: string, username: string, password: string) {
const r = await fetch(env.API_URL + '/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password, name: username })
});
const j = await r.json();
return j.token as string;
}
export async function getPosts(token: string) {
const r = await fetch(env.API_URL + '/entries', {
headers: {
Authorization: `Bearer ${token}`
}
});
const j = await r.json();
return j as Post[];
}

View File

@@ -1,2 +1,3 @@
export type { Feed } from "./types/feed.type";
export type { Post } from "./types/post.type";
export type { User } from "./types/user.type";

View File

@@ -1,7 +1,12 @@
import type { User } from "./user.type";
export type Feed = {
id: string;
title: string;
url: string;
name: string;
link: string;
faviconUrl: string;
tags: string[];
submitterId: string;
addedDate: Date;
submitter?: User;
}

View File

@@ -7,7 +7,7 @@ export type Post = {
link: string;
date: Date;
author?: string;
authors?: string[];
isRead: boolean;
isBookmarked: boolean;
isIgnored: boolean;

View File

@@ -0,0 +1,5 @@
export type User = {
id: string;
name: string;
email: string;
}

View File

@@ -0,0 +1,5 @@
<script lang="ts">
import Card from "$lib/feeds/card.svelte";
import type { Feed } from "$lib/types";
export let data;
</script>

View File

@@ -0,0 +1,23 @@
<script>
import "../app.pcss";
import { BottomNav, BottomNavItem } from "flowbite-svelte";
import { HomeSolid, UserCircleSolid } from "flowbite-svelte-icons";
</script>
<slot></slot>
<!-- <footer>
<BottomNav position="absolute" classInner="grid-cols-2">
<BottomNavItem btnName="Posts">
<HomeSolid
class="w-6 h-6 mb-1 text-gray-500 dark:text-gray-400 group-hover:text-primary-600 dark:group-hover:text-primary-500"
/>
</BottomNavItem>
<BottomNavItem btnName="Profile">
<UserCircleSolid
class="w-6 h-6 mb-1 text-gray-500 dark:text-gray-400 group-hover:text-primary-600 dark:group-hover:text-primary-500"
/>
</BottomNavItem>
</BottomNav>
</footer> -->

View File

@@ -1,6 +1,38 @@
import type { Post } from "$lib/types";
import type { Post, Feed } from "$lib/types";
import { getPosts } from "$lib/server/api";
import { get } from "svelte/store";
const data: Post[] = [
const feeds: Feed[] = [
{
id: "1",
name: "The first feed",
link: "https://example.com/feed",
faviconUrl: "https://kit.svelte.dev/favicon.png",
tags: ["tag1", "tag2"],
submitterId: "1",
addedDate: new Date(),
},
{
id: "2",
name: "Phoronix",
link: "https://www.phoronix.com",
faviconUrl: "https://www.phoronix.com/favicon.ico",
tags: ["linux", "kernel", "gnu", "gnu/linux", "gnu+linux", "gnu linux"],
submitterId: "1",
addedDate: new Date(),
},
{
id: "3",
name: "LWN.net",
link: "https://lwn.net",
faviconUrl: "https://lwn.net/favicon.ico",
tags: ["linux", "kernel", "gnu", "gnu/linux", "gnu+linux", "gnu linux"],
submitterId: "1",
addedDate: new Date(),
},
];
const posts: Post[] = [
{
id: "1",
title: "The first post",
@@ -11,14 +43,8 @@ const data: Post[] = [
isBookmarked: false,
isIgnored: false,
isReadLater: false,
author: "John Doe",
feed: {
id: "1",
title: "The first feed",
url: "https://example.com/feed",
faviconUrl: "https://kit.svelte.dev/favicon.png",
tags: ["tag1", "tag2"],
},
authors: ["John Doe"],
feed: feeds[0],
},
{
id: "2",
@@ -30,23 +56,15 @@ const data: Post[] = [
isBookmarked: true,
isIgnored: false,
isReadLater: false,
feed: {
id: "2",
title: "Phoronix",
url: "https://www.phoronix.com",
faviconUrl: "https://www.phoronix.com/favicon.ico",
// put 50 tags in the array linked with linux world
tags: [
"linux", "kernel", "gnu", "gnu/linux", "gnu+linux", "gnu linux",
"wine", "winehq", "wine-staging", "wine-devel", "winehq-devel", "winehq-staging",
"mesa", "mesa3d", "mesa 3d", "mesa-3d", "mesa3d-devel", "mesa3d-staging",
"NVK"
],
},
feed: feeds[1],
}
];
export function load() {
return { data };
export async function load({ cookies }) {
const token = cookies.get("token");
return {
posts: await getPosts(token),
feeds
};
}

View File

@@ -1,19 +1,88 @@
<script>
<script lang="ts">
export let data;
import {
BottomNav,
BottomNavItem,
Skeleton,
ImagePlaceholder,
Avatar,
Button,
P,
Search,
MultiSelect,
} from "flowbite-svelte";
import List from "$lib/posts/list.svelte";
import { CalendarWeekSolid } from "flowbite-svelte-icons";
import TimelineItem from "$lib/posts/timeline-item.svelte";
import { Timeline, TimelineItem as TI } from "flowbite-svelte";
import {
HomeSolid,
WalletSolid,
AdjustmentsVerticalOutline,
UserCircleSolid,
} from "flowbite-svelte-icons";
$: displayPosts = (data?.posts || []).map((post) => {
let dateStr = post.feed.name + " - ";
dateStr += new Date(post.date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
if (post.authors && post.authors.length > 0) {
dateStr += ` by ${post.authors.join(", ")}`;
}
return {
...post,
dateStr,
};
});
let selected: string[] = [];
let countries = [
{ value: "us", name: "United States" },
{ value: "ca", name: "Canada" },
{ value: "fr", name: "France" },
{ value: "jp", name: "Japan" },
{ value: "en", name: "England" },
];
</script>
<!-- <MultiSelect items={countries} bind:value={selected} size="lg" />
<Search>
<Button>Search</Button>
</Search> -->
<main>
<h1>Welcome to SvelteKit</h1>
<p>
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
</p>
<List posts={data.data} />
<Timeline order="vertical" class="max-w-3xl">
{#each displayPosts as post}
<TimelineItem title={post.title} date={post.dateStr} {post}>
<svelte:fragment slot="icon">
<span
class="flex absolute -start-3 justify-center items-center w-6 h-6 bg-primary-200 rounded-full ring-8 ring-white dark:ring-gray-900 dark:bg-primary-900"
>
<Avatar
src={post.feed.faviconUrl}
alt={post.feed.name}
class="w-4 h-4 -start-2 -top-2"
/>
</span>
</svelte:fragment>
<p
class="mb-4 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-2"
>
{#if post.content}
{post.content}
{/if}
</p>
</TimelineItem>
{/each}
</Timeline>
</main>
<style>
main {
margin-left: 10%;
display: flex;
width: 100vw;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,35 @@
import { fail, redirect } from '@sveltejs/kit';
import { login, register } from '$lib/server/api.js';
export const actions = {
connect: async ({ cookies, request }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
const token = await login(email as any, password as any);
cookies.set('token', token, { path: '/', maxAge: 60 * 60 * 24 * 365 * 2 });
throw redirect(303, '/');
},
register: async ({ cookies, request }) => {
const data = await request.formData();
const email = data.get('email');
const username = data.get('username');
const password = data.get('password');
const passwordRepeat = data.get('password-repeat');
console.log("register", email, username, password, passwordRepeat);
if (password !== passwordRepeat) {
return fail(400, {
data: {
email
},
errors: {
password: 'Passwords do not match'
}
})
}
const token = await register(email as any, username as any, password as any);
cookies.set('token', token, { path: '/', maxAge: 60 * 60 * 24 * 365 * 2 });
throw redirect(303, '/');
},
}

View File

@@ -0,0 +1,203 @@
<script>
import {
Label,
Input,
InputAddon,
ButtonGroup,
Button,
Heading,
Secondary,
P,
Helper,
Span,
Spinner,
} from "flowbite-svelte";
import {
EnvelopeOutline,
LockOutline,
UserOutline,
} from "flowbite-svelte-icons";
import { enhance } from "$app/forms";
export let form;
let registering = false;
let connecting = false;
</script>
<div class="text-center mb-10">
<Heading
tag="h1"
class="mb-4"
customSize="text-5xl font-extrabold md:text-5xl lg:text-6xl"
>
Welcome to <Span gradient>Vex</Span>
</Heading>
<P class="mb-4 w-full text-center">The best place to gather your feeds</P>
</div>
<div class="main">
<form
method="POST"
action="?/connect"
use:enhance={() => {
connecting = true;
return async ({ update }) => {
await update();
connecting = false;
};
}}
>
<Heading tag="h3" class="mb-4">Login</Heading>
<div>
<Label for="email" class="block mb-2">Email</Label>
<ButtonGroup class="w-full">
<InputAddon>
<EnvelopeOutline class="w-4 h-4 text-gray-500 dark:text-gray-400" />
</InputAddon>
<Input
id="email"
type="email"
name="email"
placeholder="name@flowbite.com"
required
></Input>
</ButtonGroup>
</div>
<div>
<Label for="password" class="block mb-2">Password</Label>
<ButtonGroup class="w-full">
<InputAddon>
<LockOutline class="w-4 h-4 text-gray-500 dark:text-gray-400" />
</InputAddon>
<Input id="password" type="password" name="password" required></Input>
</ButtonGroup>
</div>
<div>
<Button
type="submit"
color="primary"
disabled={connecting}
class="w-full"
>
{#if connecting}
<Spinner class="me-3" size="4" color="white" />
{/if}
Connect</Button
>
</div>
</form>
<form
method="POST"
action="?/register"
use:enhance={() => {
registering = true;
return async ({ update }) => {
await update();
registering = false;
};
}}
>
<Heading tag="h3" class="mb-4">Welcome</Heading>
<div>
<Label for="username" class="block mb-2">Your Username</Label>
<ButtonGroup class="w-full">
<InputAddon>
<UserOutline class="w-4 h-4 text-gray-500 dark:text-gray-400" />
</InputAddon>
<Input
id="username"
type="text"
name="username"
placeholder="Jean luc"
required
></Input>
</ButtonGroup>
</div>
<div>
<Label for="email" class="block mb-2">Your Email</Label>
<ButtonGroup class="w-full">
<InputAddon>
<EnvelopeOutline class="w-4 h-4 text-gray-500 dark:text-gray-400" />
</InputAddon>
<Input
id="email"
type="email"
name="email"
placeholder="name@flowbite.com"
value={form?.data?.email}
required
></Input>
</ButtonGroup>
</div>
<div>
<Label
for="password"
class="block mb-2"
color={form?.errors?.password ? "red" : undefined}>Your Password</Label
>
<ButtonGroup class="w-full">
<InputAddon>
<LockOutline class="w-4 h-4 text-gray-500 dark:text-gray-400" />
</InputAddon>
<Input
color={form?.errors?.password ? "red" : undefined}
id="password"
name="password"
type="password"
required
></Input>
</ButtonGroup>
</div>
<div>
<Label
for="password-repeat"
color={form?.errors?.password ? "red" : undefined}
class="block mb-2">Repeat Password</Label
>
<ButtonGroup class="w-full">
<InputAddon>
<LockOutline class="w-4 h-4 text-gray-500 dark:text-gray-400" />
</InputAddon>
<Input
id="password-repeat"
name="password-repeat"
type="password"
color={form?.errors?.password ? "red" : undefined}
required
></Input>
</ButtonGroup>
{#if form?.errors?.password}
<Helper class="mt-2" color="red"
><span class="font-medium">Invalid!:</span> Passwords do not match</Helper
>
{/if}
</div>
<div>
<Button
type="submit"
color="primary"
disabled={registering}
class="w-full"
>
{#if registering}
<Spinner class="me-3" size="4" color="white" />
{/if}
Register</Button
>
</div>
</form>
</div>
<style>
.main {
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
}
form {
flex-grow: 1;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>

View File

@@ -0,0 +1,62 @@
import type { Feed } from "$lib/types";
const feeds: Feed[] = [
{
id: "1",
name: "The first feed",
link: "https://example.com/feed",
faviconUrl: "https://kit.svelte.dev/favicon.png",
tags: ["tag1", "tag2"],
submitterId: "1",
addedDate: new Date(),
},
{
id: "2",
name: "Phoronix",
link: "https://www.phoronix.com",
faviconUrl: "https://www.phoronix.com/favicon.ico",
tags: ["linux", "kernel", "gnu", "gnu/linux", "gnu+linux", "gnu linux"],
submitterId: "1",
addedDate: new Date(),
},
{
id: "3",
name: "LWN.net",
link: "https://lwn.net",
faviconUrl: "https://lwn.net/favicon.ico",
tags: ["linux", "kernel", "gnu", "gnu/linux", "gnu+linux", "gnu linux"],
submitterId: "1",
addedDate: new Date(),
},
{
id: "4",
name: "Reddit",
link: "https://www.reddit.com",
faviconUrl: "https://www.reddit.com/favicon.ico",
tags: ["social", "news", "discussion"],
submitterId: "1",
addedDate: new Date(),
submitter: {
id: "1",
name: "John Doe",
email: "john.doe@gmail.com"
}
},
{
id: "5",
name: "Hacker News",
link: "https://news.ycombinator.com",
faviconUrl: "https://news.ycombinator.com/favicon.ico",
tags: ["news", "discussion"],
submitterId: "1",
addedDate: new Date(),
}
];
export function load() {
return {
feeds
};
}

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import Card from "$lib/feeds/card.svelte";
export let data;
</script>
<section>
<h1>All Feeds</h1>
<h1>Feeds</h1>
<ul>
{#each data.feeds as feed}
<Card {feed} />
{/each}
</ul>
</section>
<style>
ul {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 10px;
align-items: stretch;
justify-items: stretch;
}
</style>

View File

@@ -1,18 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import adapter from "@sveltejs/adapter-auto";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: [vitePreprocess({})],
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(),
},
};
export default config;

30
web/tailwind.config.cjs Normal file
View File

@@ -0,0 +1,30 @@
/** @type {import('tailwindcss').Config}*/
const config = {
content: ['./src/**/*.{html,js,svelte,ts}', './node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}'],
plugins: [require('flowbite/plugin')],
darkMode: 'selector',
theme: {
extend: {
colors: {
// flowbite-svelte
primary: {
50: '#FFF5F2',
100: '#FFF1EE',
200: '#FFE4DE',
300: '#FFD5CC',
400: '#FFBCAD',
500: '#FE795D',
600: '#EF562F',
700: '#EB4F27',
800: '#CC4522',
900: '#A5371B'
}
}
}
}
};
module.exports = config;