diff options
author | kartofen <mladenovnasko0@gmail.com> | 2022-08-26 23:54:17 +0300 |
---|---|---|
committer | kartofen <mladenovnasko0@gmail.com> | 2022-08-26 23:54:17 +0300 |
commit | 8f5278eb443864910dd9c2131c992d71e3af2d20 (patch) | |
tree | d56d805fa010e4b10af7dec0ed359f218e859667 /src |
Big bang
Diffstat (limited to 'src')
-rw-r--r-- | src/components/Card.astro | 76 | ||||
-rw-r--r-- | src/components/Comment.svelte | 45 | ||||
-rw-r--r-- | src/components/Thread.svelte | 74 | ||||
-rw-r--r-- | src/env.d.ts | 1 | ||||
-rw-r--r-- | src/layouts/Default.astro | 21 | ||||
-rw-r--r-- | src/layouts/Layout.astro | 56 | ||||
-rw-r--r-- | src/lib/api.ts | 14 | ||||
-rw-r--r-- | src/lib/image.ts | 10 | ||||
-rw-r--r-- | src/lib/thread.ts | 80 | ||||
-rw-r--r-- | src/lib/util.ts | 23 | ||||
-rw-r--r-- | src/models/Thread.ts | 19 | ||||
-rw-r--r-- | src/pages/404.md | 1 | ||||
-rw-r--r-- | src/pages/board/[board].astro | 26 | ||||
-rw-r--r-- | src/pages/board/[board]/[tid].astro | 26 | ||||
-rw-r--r-- | src/pages/boards.astro | 32 | ||||
-rw-r--r-- | src/pages/index.astro | 101 |
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>→</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>  + {/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> |