mirror of
https://github.com/zoriya/vex.git
synced 2025-12-06 07:06:09 +00:00
Merge pull request #10 from zoriya/my_front
Added tailwind svelte component framework
This commit is contained in:
@@ -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
12
web/Dockerfile.dev
Normal 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
6239
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
13
web/postcss.config.cjs
Normal 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;
|
||||
@@ -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
4
web/src/app.pcss
Normal file
@@ -0,0 +1,4 @@
|
||||
/* Write your global styles here, in PostCSS syntax */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
67
web/src/lib/components/tabs.svelte
Normal file
67
web/src/lib/components/tabs.svelte
Normal 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}
|
||||
51
web/src/lib/feeds/card.svelte
Normal file
51
web/src/lib/feeds/card.svelte
Normal 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>
|
||||
29
web/src/lib/feeds/side-panel.svelte
Normal file
29
web/src/lib/feeds/side-panel.svelte
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
109
web/src/lib/posts/timeline-item.svelte
Normal file
109
web/src/lib/posts/timeline-item.svelte
Normal 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
36
web/src/lib/server/api.ts
Normal 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[];
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export type Post = {
|
||||
link: string;
|
||||
date: Date;
|
||||
|
||||
author?: string;
|
||||
authors?: string[];
|
||||
isRead: boolean;
|
||||
isBookmarked: boolean;
|
||||
isIgnored: boolean;
|
||||
|
||||
5
web/src/lib/types/user.type.ts
Normal file
5
web/src/lib/types/user.type.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
5
web/src/lib/views/FeedsList.view.svelte
Normal file
5
web/src/lib/views/FeedsList.view.svelte
Normal 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>
|
||||
23
web/src/routes/+layout.svelte
Normal file
23
web/src/routes/+layout.svelte
Normal 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> -->
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
35
web/src/routes/auth/+page.server.ts
Normal file
35
web/src/routes/auth/+page.server.ts
Normal 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, '/');
|
||||
},
|
||||
}
|
||||
203
web/src/routes/auth/+page.svelte
Normal file
203
web/src/routes/auth/+page.svelte
Normal 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>
|
||||
62
web/src/routes/feeds/+page.server.ts
Normal file
62
web/src/routes/feeds/+page.server.ts
Normal 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
|
||||
};
|
||||
}
|
||||
|
||||
26
web/src/routes/feeds/+page.svelte
Normal file
26
web/src/routes/feeds/+page.svelte
Normal 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>
|
||||
@@ -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
30
web/tailwind.config.cjs
Normal 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;
|
||||
Reference in New Issue
Block a user