aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorkartofen <mladenovnasko0@gmail.com>2022-08-26 23:54:17 +0300
committerkartofen <mladenovnasko0@gmail.com>2022-08-26 23:54:17 +0300
commit8f5278eb443864910dd9c2131c992d71e3af2d20 (patch)
treed56d805fa010e4b10af7dec0ed359f218e859667 /src
Big bang
Diffstat (limited to 'src')
-rw-r--r--src/components/Card.astro76
-rw-r--r--src/components/Comment.svelte45
-rw-r--r--src/components/Thread.svelte74
-rw-r--r--src/env.d.ts1
-rw-r--r--src/layouts/Default.astro21
-rw-r--r--src/layouts/Layout.astro56
-rw-r--r--src/lib/api.ts14
-rw-r--r--src/lib/image.ts10
-rw-r--r--src/lib/thread.ts80
-rw-r--r--src/lib/util.ts23
-rw-r--r--src/models/Thread.ts19
-rw-r--r--src/pages/404.md1
-rw-r--r--src/pages/board/[board].astro26
-rw-r--r--src/pages/board/[board]/[tid].astro26
-rw-r--r--src/pages/boards.astro32
-rw-r--r--src/pages/index.astro101
16 files changed, 605 insertions, 0 deletions
diff --git a/src/components/Card.astro b/src/components/Card.astro
new file mode 100644
index 0000000..aea28c8
--- /dev/null
+++ b/src/components/Card.astro
@@ -0,0 +1,76 @@
+---
+export interface Props {
+ title: string;
+ body: string;
+ href: string;
+}
+
+const { href, title, body } = Astro.props as Props;
+---
+
+<li class="link-card">
+ <a href={href}>
+ <h2>
+ {title}
+ <span>&rarr;</span>
+ </h2>
+ <p>
+ {body}
+ </p>
+ </a>
+</li>
+<style>
+ :root {
+ --link-gradient: linear-gradient(45deg, #4f39fa, #da62c4 30%, var(--color-border) 60%);
+ }
+
+ .link-card {
+ list-style: none;
+ display: flex;
+ padding: 0.15rem;
+ background-image: var(--link-gradient);
+ background-size: 400%;
+ border-radius: 0.5rem;
+ background-position: 100%;
+ transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
+ }
+
+ .link-card > a {
+ width: 100%;
+ text-decoration: none;
+ line-height: 1.4;
+ padding: 1em 1.3em;
+ border-radius: 0.35rem;
+ color: var(--text-color);
+ background-color: white;
+ opacity: 0.8;
+ }
+
+ h2 {
+ margin: 0;
+ transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
+ }
+
+ p {
+ margin-top: 0.75rem;
+ margin-bottom: 0;
+ }
+
+ h2 span {
+ display: inline-block;
+ transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
+ }
+
+ .link-card:is(:hover, :focus-within) {
+ background-position: 0;
+ }
+
+ .link-card:is(:hover, :focus-within) h2 {
+ color: #4f39fa;
+ }
+
+ .link-card:is(:hover, :focus-within) h2 span {
+ will-change: transform;
+ transform: translateX(2px);
+ }
+</style>
diff --git a/src/components/Comment.svelte b/src/components/Comment.svelte
new file mode 100644
index 0000000..09469cc
--- /dev/null
+++ b/src/components/Comment.svelte
@@ -0,0 +1,45 @@
+<script lang="ts">
+ import { creatorColor, formatTime } from '../lib/util';
+ import type Comment from '../models/Thread';
+ export let comment: Comment;
+</script>
+
+
+<div class="commentbox" id="{comment.id}">
+
+ <span style="line-height: 2rem;">
+ {comment.id}
+ at {formatTime(comment.creationDate)} <br>
+ -> <span style="{creatorColor(comment.commentCreator)}">
+ {comment.commentCreator}
+ </span> <br>
+ </span>
+
+ {#if comment.imageId!= null && comment.imageId != undefined}
+ {#if comment.fileType == 'image'}
+ <a href="{comment.imageId}" target="_blank" rel="noopener noreferrer">
+ <img src="{comment.imageId}" alt="{comment.imageId}" height="300px" width="300px"> <br>
+ </a>
+ {:else if comment.fileType == 'video'}
+ <video width="320" height="240" controls muted>
+ <source src="{comment.imageId}">
+ </video>
+ {/if}
+ {/if}
+
+<p>{@html comment.commentText}</p>
+
+</div>
+
+<style>
+ .commentbox {
+ width: 500px;
+ border: 5px solid green;
+ padding: 10px;
+ margin: 10px;
+ background-color: white;
+ }
+ :target, .targeted {
+ background-color: #ffa;
+ }
+</style>
diff --git a/src/components/Thread.svelte b/src/components/Thread.svelte
new file mode 100644
index 0000000..8e07c9f
--- /dev/null
+++ b/src/components/Thread.svelte
@@ -0,0 +1,74 @@
+<script lang="ts">
+ import { creatorColor, formatTime } from '../lib/util';
+ import type Thread from '../models/Thread';
+ export let thread: Thread;
+ export let board: string;
+
+ let replies: string[] = [];
+ const listReplies = (id: string): boolean => {
+ replies = [];
+ thread.comments.forEach(comment => {
+ if(comment.commentText.includes(id))
+ replies.push(comment.id);
+ })
+
+ if(replies.length <= 0) return false;
+ return true
+ }
+</script>
+
+
+{#if thread.id != "rules"}
+ <div class="threadbox" id="{thread.id}">
+ <span style="line-height: 2rem;">
+ <a href="/board/{board}/{thread.id}">{thread.id}</a>
+ at {formatTime(thread.creationDate)} <br>
+
+ -> <span style="{creatorColor(thread.threadCreator )}">
+ {thread.threadCreator}
+ </span > <br>
+
+ {#if listReplies(thread.id)}
+ <hr> <span style=" display:inline-block; line-height: 2ch; font-size: 0.8rem; font-family: monospace;">
+ {#each replies as id}
+ <a href="/board/{board}/{thread.id}#{id}" onmouseover="document.getElementById('{id}').classList.add('targeted')" onmouseleave="document.getElementById('{id}').classList.remove('targeted')">>>{id}</a> &#xfeff
+ {/each}
+ </span> <hr>
+ {/if}
+
+ </span>
+
+ <h3>{thread.threadName}</h3>
+
+ {#if thread.imageId!= null && thread.imageId != undefined}
+ {#if thread.fileType == 'image'}
+ <a href="{thread.imageId}" target="_blank" rel="noopener noreferrer">
+ <img src="{thread.imageId}" alt="{thread.imageId}" height="300px" width="300px"> <br>
+ </a>
+ {:else if thread.fileType == 'video'}
+ <video width="320" height="240" controls muted>
+ <source src="{thread.imageId}">
+ </video>
+ {/if}
+ {/if}
+
+<p>{@html thread.threadText}</p>
+
+<slot />
+
+ </div>
+{/if}
+
+<style>
+.threadbox {
+ width: 600px;
+ border: 10px solid green;
+ padding: 10px;
+ margin: 10px;
+ margin-left: 0px;
+ background-color: white;
+}
+:target, .targeted {
+ background-color: #ffa;
+}
+</style>
diff --git a/src/env.d.ts b/src/env.d.ts
new file mode 100644
index 0000000..f964fe0
--- /dev/null
+++ b/src/env.d.ts
@@ -0,0 +1 @@
+/// <reference types="astro/client" />
diff --git a/src/layouts/Default.astro b/src/layouts/Default.astro
new file mode 100644
index 0000000..85fd510
--- /dev/null
+++ b/src/layouts/Default.astro
@@ -0,0 +1,21 @@
+---
+---
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ </head>
+ <body>
+ <slot />
+ </body>
+<html>
+
+<style>
+ html {
+ font-family: sans-serif;
+ padding-left: 1em;
+ }
+</style>
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
new file mode 100644
index 0000000..0e3e837
--- /dev/null
+++ b/src/layouts/Layout.astro
@@ -0,0 +1,56 @@
+---
+export interface Props {
+ title: string;
+}
+
+const { title } = Astro.props as Props;
+---
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="generator" content={Astro.generator} />
+ <title>{title}</title>
+ </head>
+ <body>
+ <slot />
+ </body>
+</html>
+<style>
+ :root {
+ --font-size-base: clamp(1rem, 0.34vw + 0.91rem, 1.19rem);
+ --font-size-lg: clamp(1.2rem, 0.7vw + 1.2rem, 1.5rem);
+ --font-size-xl: clamp(2.44rem, 2.38vw + 1.85rem, 3.75rem);
+
+ --color-text: hsl(12, 5%, 4%);
+ --color-bg: hsl(10, 21%, 95%);
+ --color-border: hsl(17, 24%, 90%);
+ }
+
+ html {
+ font-family: system-ui, sans-serif;
+ font-size: var(--font-size-base);
+ color: var(--color-text);
+ background-color: var(--color-bg);
+ }
+
+ body {
+ margin: 0;
+ }
+
+ :global(h1) {
+ font-size: var(--font-size-xl);
+ }
+
+ :global(h2) {
+ font-size: var(--font-size-lg);
+ }
+
+ :global(code) {
+ font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
+ Bitstream Vera Sans Mono, Courier New, monospace;
+ }
+</style>
diff --git a/src/lib/api.ts b/src/lib/api.ts
new file mode 100644
index 0000000..221a7b3
--- /dev/null
+++ b/src/lib/api.ts
@@ -0,0 +1,14 @@
+const base = 'https://localhost:5001/api'
+
+export async function api(method: string, resource: string, data?: any) {
+ console.log("API USED", method, resource, data);
+
+
+ return await fetch(`${base}/${method}/${resource}`, {
+ method,
+ headers: {
+ 'content-type': 'application/json'
+ },
+ body: data
+ });
+}
diff --git a/src/lib/image.ts b/src/lib/image.ts
new file mode 100644
index 0000000..53d4c98
--- /dev/null
+++ b/src/lib/image.ts
@@ -0,0 +1,10 @@
+import { api } from './api';
+
+export async function getImg(imgId: string) {
+ if(imgId == null || imgId == '' || imgId == undefined) return null;
+
+ const response = await api('get', `image/${imgId}`)
+
+ if(response.status !== 200) return `Server error: ${response.status}`;
+ return response.text();
+}
diff --git a/src/lib/thread.ts b/src/lib/thread.ts
new file mode 100644
index 0000000..d0a4fce
--- /dev/null
+++ b/src/lib/thread.ts
@@ -0,0 +1,80 @@
+import { writeFileSync, existsSync } from 'fs';
+import { getImg } from './image';
+import type Thread from '../models/Thread'
+
+export async function processThreadIn(board: string, thread: Thread, procComments?: false) {
+ if(!thread || !board) return;
+ let imageId: string = thread.imageId;
+
+ if(existsSync(`public/images/${imageId}`))
+ thread.imageId = `/images/${imageId}`;
+ else
+ thread.imageId = await getImg(imageId);
+
+ thread.threadText = replaceURLs(thread.threadText, board, thread.id);
+
+ if(!procComments) return;
+
+ for(let comment of thread.comments)
+ {
+ let cimageId = comment.imageId;
+ if(existsSync(`public/images/${cimageId}`))
+ comment.imageId = `/images/${cimageId}`;
+ else
+ comment.imageId = await getImg(cimageId);
+
+ comment.commentText = replaceURLs(comment.commentText, board, thread.id);
+ }
+}
+
+function replaceURLs(text: string, board: string, OPtid?: string): string {
+ if(!text) return;
+
+ if(OPtid) {
+ let replyRegex = /(^|[^>])>>[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/g;
+ text = text.replace(replyRegex, (id) => {
+ let showid = id;
+ id = id.replace(/(>|\n| )/g, '');
+ if(id == OPtid) showid += "(OP)";
+ // this adds the targeted class
+ // to the target id when its hovered on
+ // and removes it when it stops hovering
+ return ` <a class="${id}" href="#${id}" onmouseover="document.getElementById('${id}').classList.add('targeted')" onmouseleave="document.getElementById('${id}').classList.remove('targeted')"> ${showid} </a> `;
+ });
+ }
+
+ // link to thread on other board
+ let otherthreadlinkRegex = />>>\/.*\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/g;
+ text = text.replace(otherthreadlinkRegex, (id) => {
+ let link = id.slice(4);
+ let parts = link.split('/');
+ let linkid = parts[1]; let board = parts[0];
+ return ` <a href="${board}/${linkid}#{linkid}"> >>>/${board}/${linkid} </a> `
+ });
+ // link to thread on this board
+ let threadlinkRegex = />>>[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/g;
+ text = text.replace(threadlinkRegex, (id) => {
+ let linkid = id.slice(3); // remove the first >>>
+ return ` <a href="../${board}/${linkid}#${linkid}"> ${id} </a> `
+ });
+ // link to another board
+ let otherboardRegex = />>>\/.*\/($|[^0-9a-fA-F])/g;
+ text = text.replace(otherboardRegex, (id) => {
+ let boardid = id.split('/')[1];
+ return ` <a href="../${boardid}"> >>>/${boardid}/ </a> `
+ });
+
+ let newlineRegex = /(\r\n|\r|\n)/g;
+ text = text.replace(newlineRegex, () => {
+ return ' <br> '
+ });
+
+ let urlRegex = /(((https?:\/\/)|(www\.))[^\s]+)/g;
+ return text.replace(urlRegex, (url) => {
+ var hyperlink = url;
+ if (!hyperlink.match('^https?:\/\/')) {
+ hyperlink = 'http://' + hyperlink;
+ }
+ return ' <a href="' + hyperlink + '" target="_blank" rel="noopener noreferrer">' + url + '</a> '
+ });
+}
diff --git a/src/lib/util.ts b/src/lib/util.ts
new file mode 100644
index 0000000..a0734b9
--- /dev/null
+++ b/src/lib/util.ts
@@ -0,0 +1,23 @@
+export const creatorColor = (creatorid: string) => {
+ let parts = creatorid.split('-')
+ let ints = parts.map(function(d) { return parseInt(d,16) })
+ let code = ints[0]
+
+ let blue = (code >> 16) & 31;
+ let green = (code >> 21) & 31;
+ let red = (code >> 27) & 31;
+ let foreColor = `border:5px solid rgb(${red << 3}, ${green << 3}, ${blue << 3});`;
+ return foreColor;
+};
+
+export function formatTime(timestamp: number): string
+{
+ let date = new Date(timestamp);
+ let year = date.getFullYear();
+ let month = date.getMonth() + 1;
+ let day = date.getDate();
+ let time = date.toTimeString().split(' ')[0];
+ let formattedTime = `${time}, ${day}/${month}/${year}`
+
+ return formattedTime;
+}
diff --git a/src/models/Thread.ts b/src/models/Thread.ts
new file mode 100644
index 0000000..e5d9f66
--- /dev/null
+++ b/src/models/Thread.ts
@@ -0,0 +1,19 @@
+export type Comment = {
+ id: string;
+ commentCreator: string;
+ commentText: string;
+ creationDate: number;
+ imageId: string;
+ fileType: string //image or video
+};
+
+export type Thread = {
+ id: string;
+ threadName: string;
+ threadCreator: string;
+ threadText: string;
+ comments: Comment[];
+ creationDate: number;
+ imageId: string;
+ fileType: string; //image or video
+};
diff --git a/src/pages/404.md b/src/pages/404.md
new file mode 100644
index 0000000..3061d00
--- /dev/null
+++ b/src/pages/404.md
@@ -0,0 +1 @@
+# 404 Error
diff --git a/src/pages/board/[board].astro b/src/pages/board/[board].astro
new file mode 100644
index 0000000..2624fed
--- /dev/null
+++ b/src/pages/board/[board].astro
@@ -0,0 +1,26 @@
+---
+import Default from '../../layouts/Default.astro';
+import Thread from '../../components/Thread.svelte'
+import type Thread from '../../models/Thread';
+
+import { api } from '../../lib/api.ts';
+import { processThreadIn } from '../../lib/thread'
+
+const { board } = Astro.params;
+const data = await api('get', `board/${board}`);
+
+if(data.status === 404) return Astro.redirect('/404');
+
+const threads: Thread[] = await data.json();
+for(let thread of threads)
+ await processThreadIn(board, thread);
+---
+
+<Default>
+ <h1><a href="/boards"> {board} </a></h1>
+
+ {threads.map((thread) => (
+ <Thread thread={thread} board={board} />
+ ))}
+
+</Default>
diff --git a/src/pages/board/[board]/[tid].astro b/src/pages/board/[board]/[tid].astro
new file mode 100644
index 0000000..80e7fbe
--- /dev/null
+++ b/src/pages/board/[board]/[tid].astro
@@ -0,0 +1,26 @@
+---
+import Default from '../../../layouts/Default.astro';
+import Thread from '../../../components/Thread.svelte'
+import Comment from '../../../components/Comment.svelte'
+import type Thread from '../../../models/Thread';
+
+import { api } from '../../../lib/api';
+import { processThreadIn } from '../../../lib/thread';
+
+const { board } = Astro.params;
+const data = await api('get', `thread/${board}/${Astro.params.tid}`);
+
+if(data.status === 404) return Astro.redirect('/404');
+
+const thread: Thread = await data.json();
+await processThreadIn(board, thread, true);
+const comments: Comment[] = thread.comments;
+---
+
+<Default>
+ <Thread thread={thread} board={board}>
+ {comments.map((comment) => (
+ <Comment comment={comment} />
+ ))}
+ </Thread>
+</Default>
diff --git a/src/pages/boards.astro b/src/pages/boards.astro
new file mode 100644
index 0000000..ab3158f
--- /dev/null
+++ b/src/pages/boards.astro
@@ -0,0 +1,32 @@
+---
+import Default from '../layouts/Default.astro';
+import { api } from '../lib/api.ts';
+
+const data = await api('get', 'boards');
+const boards: string[] = await data.json();
+---
+
+<Default>
+ <div class="blackbox">
+ <h3>Boards</h3>
+
+ <ul>
+ {boards.map((board) => (
+ <li><a href=`/board/${board}`>{board}</a></li>
+ ))}
+ </ul>
+ </div>
+</Default>
+
+<style>
+ .blackbox {
+ width: 300px;
+ height: 210px;
+ border: 10px solid black;
+ padding: 10px;
+ padding-top: 0px;
+ margin: 10px;
+ margin-left: 0px;
+ background-color: white;
+ }
+</style>
diff --git a/src/pages/index.astro b/src/pages/index.astro
new file mode 100644
index 0000000..4dfffe3
--- /dev/null
+++ b/src/pages/index.astro
@@ -0,0 +1,101 @@
+---
+import Layout from '../layouts/Layout.astro';
+import Card from '../components/Card.astro';
+---
+
+<Layout title="Welcome to Astro.">
+ <main>
+ <h1>Welcome to <span class="text-gradient">Astro</span></h1>
+ <p class="instructions">
+ Check out the <code>src/pages</code> directory to get started.<br />
+ <strong>Code Challenge:</strong> Tweak the "Welcome to Astro" message above.
+ </p>
+ <ul role="list" class="link-card-grid">
+ <Card
+ href="https://docs.astro.build/"
+ title="Documentation"
+ body="Learn how Astro works and explore the official API docs."
+ />
+ <Card
+ href="https://astro.build/integrations/"
+ title="Integrations"
+ body="Supercharge your project with new frameworks and libraries."
+ />
+ <Card
+ href="https://astro.build/themes/"
+ title="Themes"
+ body="Explore a galaxy of community-built starter themes."
+ />
+ <Card
+ href="https://astro.build/chat/"
+ title="Chat"
+ body="Come say hi to our amazing Discord community. ❤️"
+ />
+ <Card
+ href="boards"
+ title="Boards page"
+ body="Fuck you"
+ />
+ </ul>
+ </main>
+</Layout>
+
+<style>
+ :root {
+ --astro-gradient: linear-gradient(0deg, #4f39fa, #da62c4);
+ }
+
+ h1 {
+ margin: 2rem 0;
+ }
+
+ main {
+ margin: auto;
+ padding: 1em;
+ max-width: 60ch;
+ }
+
+ .text-gradient {
+ font-weight: 900;
+ background-image: var(--astro-gradient);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-size: 100% 200%;
+ background-position-y: 100%;
+ border-radius: 0.4rem;
+ animation: pulse 4s ease-in-out infinite;
+ }
+
+ @keyframes pulse {
+ 0%,
+ 100% {
+ background-position-y: 0%;
+ }
+ 50% {
+ background-position-y: 80%;
+ }
+ }
+
+ .instructions {
+ line-height: 1.6;
+ margin: 1rem 0;
+ background: #4f39fa;
+ padding: 1rem;
+ border-radius: 0.4rem;
+ color: var(--color-bg);
+ }
+
+ .instructions code {
+ font-size: 0.875em;
+ border: 0.1em solid var(--color-border);
+ border-radius: 4px;
+ padding: 0.15em 0.25em;
+ }
+
+ .link-card-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(24ch, 1fr));
+ gap: 1rem;
+ padding: 0;
+ }
+</style>