feat: le guestbook has been added
This commit is contained in:
parent
755c578e98
commit
5fce840adc
7
.gitignore
vendored
7
.gitignore
vendored
@ -22,3 +22,10 @@ vite.config.ts.timestamp-*
|
|||||||
|
|
||||||
# nix
|
# nix
|
||||||
/result
|
/result
|
||||||
|
|
||||||
|
# scala
|
||||||
|
target
|
||||||
|
.metals
|
||||||
|
.bloop
|
||||||
|
guestbook/entries
|
||||||
|
guestbook/entries_size
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -5,5 +5,8 @@
|
|||||||
},
|
},
|
||||||
"editor.quickSuggestions": {
|
"editor.quickSuggestions": {
|
||||||
"strings": "on"
|
"strings": "on"
|
||||||
|
},
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/target": true
|
||||||
}
|
}
|
||||||
}
|
}
|
1
guestbook/.bsp/sbt.json
Normal file
1
guestbook/.bsp/sbt.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"name":"sbt","version":"1.10.1","bspVersion":"2.1.0-M1","languages":["scala"],"argv":["C:\\Users\\dusk\\scoop\\apps\\graalvm-oracle-jdk\\current/bin/java","-Xms100m","-Xmx100m","-classpath","C:\\Users\\dusk\\AppData\\Local\\Coursier\\cache\\arc\\https\\github.com\\sbt\\sbt\\releases\\download\\v1.10.1\\sbt-1.10.1.zip\\sbt\\bin\\sbt-launch.jar","-Dsbt.script=C:\\Users\\dusk\\AppData\\Local\\Coursier\\data\\bin\\sbt.bat","xsbt.boot.Boot","-bsp"]}
|
2
guestbook/.scalafmt.conf
Normal file
2
guestbook/.scalafmt.conf
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
version = "3.7.15"
|
||||||
|
runner.dialect = scala3
|
29
guestbook/build.sbt
Normal file
29
guestbook/build.sbt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
val Http4sVersion = "0.23.27"
|
||||||
|
val CirceVersion = "0.14.9"
|
||||||
|
val MunitVersion = "1.0.0"
|
||||||
|
val LogbackVersion = "1.5.6"
|
||||||
|
val MunitCatsEffectVersion = "2.0.0"
|
||||||
|
|
||||||
|
lazy val root = (project in file("."))
|
||||||
|
.settings(
|
||||||
|
organization := "systems.gaze",
|
||||||
|
name := "guestbook",
|
||||||
|
version := "0.0.1-SNAPSHOT",
|
||||||
|
scalaVersion := "3.4.2",
|
||||||
|
libraryDependencies ++= Seq(
|
||||||
|
"org.http4s" %% "http4s-ember-server" % Http4sVersion,
|
||||||
|
"org.http4s" %% "http4s-circe" % Http4sVersion,
|
||||||
|
"org.http4s" %% "http4s-dsl" % Http4sVersion,
|
||||||
|
"org.scalameta" %% "munit" % MunitVersion % Test,
|
||||||
|
"org.typelevel" %% "munit-cats-effect" % MunitCatsEffectVersion % Test,
|
||||||
|
"ch.qos.logback" % "logback-classic" % LogbackVersion % Runtime,
|
||||||
|
"com.lihaoyi" %% "os-lib" % "0.9.1",
|
||||||
|
"io.circe" %% "circe-core" % CirceVersion,
|
||||||
|
"io.circe" %% "circe-generic" % CirceVersion,
|
||||||
|
"io.circe" %% "circe-parser" % CirceVersion,
|
||||||
|
),
|
||||||
|
assembly / assemblyMergeStrategy := {
|
||||||
|
case "module-info.class" => MergeStrategy.discard
|
||||||
|
case x => (assembly / assemblyMergeStrategy).value.apply(x)
|
||||||
|
}
|
||||||
|
)
|
1
guestbook/project/build.properties
Normal file
1
guestbook/project/build.properties
Normal file
@ -0,0 +1 @@
|
|||||||
|
sbt.version=1.10.1
|
8
guestbook/project/metals.sbt
Normal file
8
guestbook/project/metals.sbt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// format: off
|
||||||
|
// DO NOT EDIT! This file is auto-generated.
|
||||||
|
|
||||||
|
// This file enables sbt-bloop to create bloop config files.
|
||||||
|
|
||||||
|
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.6.0")
|
||||||
|
|
||||||
|
// format: on
|
3
guestbook/project/plugins.sbt
Normal file
3
guestbook/project/plugins.sbt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.1")
|
||||||
|
addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0")
|
||||||
|
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.2.0")
|
8
guestbook/project/project/metals.sbt
Normal file
8
guestbook/project/project/metals.sbt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// format: off
|
||||||
|
// DO NOT EDIT! This file is auto-generated.
|
||||||
|
|
||||||
|
// This file enables sbt-bloop to create bloop config files.
|
||||||
|
|
||||||
|
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.6.0")
|
||||||
|
|
||||||
|
// format: on
|
8
guestbook/project/project/project/metals.sbt
Normal file
8
guestbook/project/project/project/metals.sbt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// format: off
|
||||||
|
// DO NOT EDIT! This file is auto-generated.
|
||||||
|
|
||||||
|
// This file enables sbt-bloop to create bloop config files.
|
||||||
|
|
||||||
|
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.6.0")
|
||||||
|
|
||||||
|
// format: on
|
16
guestbook/src/main/resources/logback.xml
Normal file
16
guestbook/src/main/resources/logback.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<configuration>
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<!-- On Windows machines setting withJansi to true enables ANSI
|
||||||
|
color code interpretation by the Jansi library. This requires
|
||||||
|
org.fusesource.jansi:jansi:1.8 on the class path. Note that
|
||||||
|
Unix-based operating systems such as Linux and Mac OS X
|
||||||
|
support ANSI color codes by default. -->
|
||||||
|
<withJansi>true</withJansi>
|
||||||
|
<encoder>
|
||||||
|
<pattern>[%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="STDOUT" />
|
||||||
|
</root>
|
||||||
|
</configuration>
|
@ -0,0 +1,65 @@
|
|||||||
|
package systems.gaze.guestbook
|
||||||
|
|
||||||
|
import cats.Applicative
|
||||||
|
import cats.syntax.all.*
|
||||||
|
import io.circe.Decoder
|
||||||
|
import io.circe.generic.auto._, io.circe.syntax._, io.circe._, io.circe.parser._
|
||||||
|
import os.Path
|
||||||
|
|
||||||
|
trait Guestbook[F[_]]:
|
||||||
|
def read(config: Guestbook.Config, from: Int, count: Int): F[Guestbook.Page]
|
||||||
|
def write(config: Guestbook.Config, entry: Guestbook.Entry): F[Unit]
|
||||||
|
|
||||||
|
object Guestbook:
|
||||||
|
private val logger = org.log4s.getLogger
|
||||||
|
|
||||||
|
final case class Config(
|
||||||
|
val entriesPath: Path,
|
||||||
|
val entryCountPath: Path,
|
||||||
|
)
|
||||||
|
final case class Entry(
|
||||||
|
val author: String,
|
||||||
|
val content: String,
|
||||||
|
val timestamp: Long
|
||||||
|
)
|
||||||
|
final case class Page(
|
||||||
|
val entries: List[Tuple2[Int, Entry]],
|
||||||
|
val hasNext: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
def impl[F[_]: Applicative]: Guestbook[F] = new Guestbook[F]:
|
||||||
|
def entriesSize(config: Config): Int =
|
||||||
|
os.read(config.entryCountPath).toInt
|
||||||
|
def read(config: Config, from: Int, count: Int): F[Page] =
|
||||||
|
val entryCount = entriesSize(config)
|
||||||
|
// limit from to 1 cuz duh lol
|
||||||
|
val startFrom = from.max(1)
|
||||||
|
// limit count to however many entries there are
|
||||||
|
val endAt = (startFrom + count - 1).min(entryCount).max(1)
|
||||||
|
// if it wants us to start from after entries just return empty
|
||||||
|
if startFrom > entryCount then
|
||||||
|
return Page(entries = List.empty, hasNext = false).pure[F]
|
||||||
|
logger.trace(s"want to read entries from $startFrom ($from) to $endAt ($from + $count)")
|
||||||
|
// actually get the entries
|
||||||
|
val entries = (startFrom to endAt)
|
||||||
|
.map((no) => // read the entries
|
||||||
|
val entryNo = entryCount - no + 1
|
||||||
|
logger.trace(s"reading entry at $entryNo")
|
||||||
|
entryNo -> decode[Entry](os.read(config.entriesPath / entryNo.toString)).getOrElse(
|
||||||
|
Entry(
|
||||||
|
author = "error",
|
||||||
|
content = "woops, this is an error!",
|
||||||
|
timestamp = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toList
|
||||||
|
Page(entries, hasNext = entryCount > endAt).pure[F]
|
||||||
|
def write(config: Config, entry: Entry): F[Unit] =
|
||||||
|
val entryNo = entriesSize(config) + 1
|
||||||
|
val entryPath = config.entriesPath / entryNo.toString
|
||||||
|
// write entry
|
||||||
|
os.write.over(entryPath, entry.asJson.toString)
|
||||||
|
// update entry count
|
||||||
|
os.write.over(config.entryCountPath, entryNo.toString)
|
||||||
|
().pure[F]
|
@ -0,0 +1,66 @@
|
|||||||
|
package systems.gaze.guestbook
|
||||||
|
|
||||||
|
import io.circe.generic.auto._
|
||||||
|
import org.http4s.HttpRoutes
|
||||||
|
import org.http4s.dsl.Http4sDsl
|
||||||
|
import org.http4s.circe.CirceEntityEncoder.circeEntityEncoder
|
||||||
|
import org.http4s.UrlForm
|
||||||
|
import org.http4s.headers.Location
|
||||||
|
import cats.effect.IO
|
||||||
|
import cats.implicits.*
|
||||||
|
import org.http4s.server.middleware.Throttle
|
||||||
|
import scala.concurrent.duration.DurationInt
|
||||||
|
import cats.effect.unsafe.implicits.global
|
||||||
|
import scala.concurrent.duration.FiniteDuration
|
||||||
|
import org.http4s.Uri
|
||||||
|
|
||||||
|
class GuestbookRoutes(var guestbookConfig: Guestbook.Config, var websiteUri: Uri):
|
||||||
|
val dsl = new Http4sDsl[IO] {}
|
||||||
|
import dsl.*
|
||||||
|
|
||||||
|
def throttle(
|
||||||
|
ratelimited: String,
|
||||||
|
amount: Int,
|
||||||
|
per: FiniteDuration
|
||||||
|
)(routes: HttpRoutes[IO]): HttpRoutes[IO] =
|
||||||
|
Throttle
|
||||||
|
.httpRoutes[IO](amount, per)(routes)
|
||||||
|
.unsafeRunSync()
|
||||||
|
.map((resp) =>
|
||||||
|
// respond with SeeOther and put ratelimited query param
|
||||||
|
if (resp.status == TooManyRequests)
|
||||||
|
resp
|
||||||
|
.withStatus(SeeOther)
|
||||||
|
.withHeaders(
|
||||||
|
Location(
|
||||||
|
(websiteUri / "guestbook")
|
||||||
|
.withQueryParam("ratelimited", ratelimited)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
resp
|
||||||
|
)
|
||||||
|
|
||||||
|
def routes(
|
||||||
|
G: Guestbook[IO]
|
||||||
|
): HttpRoutes[IO] =
|
||||||
|
val putEntry = HttpRoutes.of[IO] {
|
||||||
|
case req @ POST -> Root => for {
|
||||||
|
entry <- req.as[UrlForm].map { form =>
|
||||||
|
val author = form.getFirstOrElse("author", "error")
|
||||||
|
val content = form.getFirstOrElse("content", "error")
|
||||||
|
Guestbook
|
||||||
|
.Entry(author, content, timestamp = System.currentTimeMillis / 1000)
|
||||||
|
}
|
||||||
|
result <- G.write(guestbookConfig, entry)
|
||||||
|
resp <- SeeOther(Location(websiteUri / "guestbook"))
|
||||||
|
} yield resp
|
||||||
|
}
|
||||||
|
val getEntries = HttpRoutes.of[IO] {
|
||||||
|
case GET -> Root / IntVar(page) => for {
|
||||||
|
entries <- G.read(guestbookConfig, (page - 1).max(0) * 5, 5)
|
||||||
|
resp <- Ok(entries)
|
||||||
|
} yield resp
|
||||||
|
}
|
||||||
|
throttle("get", 10, 2.seconds)(getEntries)
|
||||||
|
<+> throttle("send", 5, 10.seconds)(putEntry)
|
@ -0,0 +1,35 @@
|
|||||||
|
package systems.gaze.guestbook
|
||||||
|
|
||||||
|
import com.comcast.ip4s.*
|
||||||
|
import fs2.io.net.Network
|
||||||
|
import org.http4s.ember.server.EmberServerBuilder
|
||||||
|
import org.http4s.implicits.*
|
||||||
|
import org.http4s.server.middleware.Logger
|
||||||
|
import cats.effect.IO
|
||||||
|
import org.http4s.Uri
|
||||||
|
|
||||||
|
object GuestbookServer:
|
||||||
|
|
||||||
|
def run(
|
||||||
|
guestbookConfig: Guestbook.Config,
|
||||||
|
websiteUri: Uri,
|
||||||
|
): IO[Nothing] = {
|
||||||
|
val guestbookAlg = Guestbook.impl[IO]
|
||||||
|
|
||||||
|
// Combine Service Routes into an HttpApp.
|
||||||
|
// Can also be done via a Router if you
|
||||||
|
// want to extract segments not checked
|
||||||
|
// in the underlying routes.
|
||||||
|
val httpApp =
|
||||||
|
(GuestbookRoutes(guestbookConfig, websiteUri).routes(guestbookAlg)).orNotFound
|
||||||
|
|
||||||
|
// With Middlewares in place
|
||||||
|
val finalHttpApp = Logger.httpApp(true, true)(httpApp)
|
||||||
|
|
||||||
|
EmberServerBuilder
|
||||||
|
.default[IO]
|
||||||
|
.withHost(ipv4"0.0.0.0")
|
||||||
|
.withPort(port"8080")
|
||||||
|
.withHttpApp(finalHttpApp)
|
||||||
|
.build
|
||||||
|
}.useForever
|
20
guestbook/src/main/scala/systems/gaze/guestbook/Main.scala
Normal file
20
guestbook/src/main/scala/systems/gaze/guestbook/Main.scala
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package systems.gaze.guestbook
|
||||||
|
|
||||||
|
import cats.effect.IOApp
|
||||||
|
import org.http4s.Uri
|
||||||
|
|
||||||
|
object Main extends IOApp.Simple:
|
||||||
|
val websiteUri =
|
||||||
|
Uri
|
||||||
|
.fromString(sys.env("GUESTBOOK_WEBSITE_URI"))
|
||||||
|
.getOrElse(throw new Exception("write better uris lol"))
|
||||||
|
val guestbookConfig = Guestbook.Config(
|
||||||
|
entriesPath = os.pwd / "entries",
|
||||||
|
entryCountPath = os.pwd / "entries_size"
|
||||||
|
)
|
||||||
|
// make the entries path if it doesnt exist
|
||||||
|
os.makeDir.all(guestbookConfig.entriesPath)
|
||||||
|
// make the entry count path if it doesnt exist
|
||||||
|
if !os.exists(guestbookConfig.entryCountPath) then
|
||||||
|
os.write(guestbookConfig.entryCountPath, 0.toString)
|
||||||
|
val run = GuestbookServer.run(guestbookConfig, websiteUri)
|
@ -16,8 +16,10 @@
|
|||||||
"@sveltejs/adapter-static": "^3.0.2",
|
"@sveltejs/adapter-static": "^3.0.2",
|
||||||
"@sveltejs/kit": "^2.5.20",
|
"@sveltejs/kit": "^2.5.20",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||||
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@tailwindcss/typography": "^0.5.14",
|
"@tailwindcss/typography": "^0.5.14",
|
||||||
"@types/eslint": "^8.56.11",
|
"@types/eslint": "^8.56.11",
|
||||||
|
"@types/node": "^22.4.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.8.0",
|
"eslint": "^9.8.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
25
src/lib/fetchHack.mjs
Normal file
25
src/lib/fetchHack.mjs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// hack/postSync.mjs
|
||||||
|
/**
|
||||||
|
,* @file POST some stuff to a URL.
|
||||||
|
,* Usage: one of
|
||||||
|
,* echo input | node postSync.mjs <url>
|
||||||
|
,* node postSync.mjs <url> <input>
|
||||||
|
,*/
|
||||||
|
|
||||||
|
// The argument count would break if called as a standalone script.
|
||||||
|
const url = process.argv[2] || process.exit(1);
|
||||||
|
|
||||||
|
const response = await fetch(url, {method:'GET',redirect:'manual'});
|
||||||
|
const json = await response.text().then(text => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
return data
|
||||||
|
} catch(err) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
location: response.headers.get('location'),
|
||||||
|
status: response.status,
|
||||||
|
body: json,
|
||||||
|
}));
|
@ -1,7 +1,7 @@
|
|||||||
const getTitle = (path: string) => {
|
const getTitle = (path: string) => {
|
||||||
let sl = path.split('/')
|
let sl = path.split('/')
|
||||||
sl = sl.splice(1)
|
sl = sl.splice(1)
|
||||||
if (sl.length > 1) {
|
if (sl.length > 2) {
|
||||||
sl[0] = sl[0][0]
|
sl[0] = sl[0][0]
|
||||||
}
|
}
|
||||||
const newPath = sl.join('/')
|
const newPath = sl.join('/')
|
||||||
|
@ -14,7 +14,8 @@
|
|||||||
let menuItems: MenuItem[] = [
|
let menuItems: MenuItem[] = [
|
||||||
{ href: '', name: 'home', iconUri: '/icons/home.png' },
|
{ href: '', name: 'home', iconUri: '/icons/home.png' },
|
||||||
{ href: 'entries', name: 'entries', iconUri: '/icons/entries.png' },
|
{ href: 'entries', name: 'entries', iconUri: '/icons/entries.png' },
|
||||||
{ href: 'about', name: 'about', iconUri: '/icons/about.png' }
|
{ href: 'guestbook', name: 'guestbook', iconUri: '/icons/guestbook.png' },
|
||||||
|
{ href: 'about', name: 'about', iconUri: '/icons/about.png' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const routeComponents = data.route.split('/');
|
const routeComponents = data.route.split('/');
|
||||||
|
58
src/routes/guestbook/+page.server.ts
Normal file
58
src/routes/guestbook/+page.server.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { GUESTBOOK_URL } from '$env/static/private'
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import {spawnSync} from 'node:child_process'
|
||||||
|
|
||||||
|
interface Entry {
|
||||||
|
author: String,
|
||||||
|
content: String,
|
||||||
|
timestamp: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FetchResult {
|
||||||
|
location: string,
|
||||||
|
status: number,
|
||||||
|
body: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchBlocking(url: string): FetchResult | string {
|
||||||
|
const spawnResult = spawnSync("bun", ["src/lib/fetchHack.mjs", url]);
|
||||||
|
const out = spawnResult.stdout.toString();
|
||||||
|
try {
|
||||||
|
return JSON.parse(out)
|
||||||
|
} catch(err: any) {
|
||||||
|
return spawnResult.stderr.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function load({ url }) {
|
||||||
|
var data = {
|
||||||
|
entries: [] as [number, Entry][],
|
||||||
|
guestbook_url: GUESTBOOK_URL,
|
||||||
|
ratelimitedFeat: url.searchParams.get('ratelimited') as string || "",
|
||||||
|
page: parseInt(url.searchParams.get('page') || "1") || 1,
|
||||||
|
hasNext: false,
|
||||||
|
fetchError: "",
|
||||||
|
}
|
||||||
|
// handle cases where the page query might be a string so we just return back page 1 instead
|
||||||
|
data.page = isNaN(data.page) ? 1 : data.page
|
||||||
|
data.page = Math.max(data.page, 1)
|
||||||
|
if (data.ratelimitedFeat === "get") {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
const entriesResp = fetchBlocking(GUESTBOOK_URL + "/" + data.page)
|
||||||
|
if (typeof entriesResp === "string") {
|
||||||
|
data.fetchError = entriesResp
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
const locationRaw = entriesResp.status === 303 ? entriesResp.location : null
|
||||||
|
if (locationRaw !== null && locationRaw.length > 0) {
|
||||||
|
const location = new URL(locationRaw)
|
||||||
|
data.ratelimitedFeat = location.searchParams.get('ratelimited') as string || ""
|
||||||
|
}
|
||||||
|
if (data.ratelimitedFeat === "get") {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
data.entries = entriesResp.body.entries
|
||||||
|
data.hasNext = entriesResp.body.hasNext
|
||||||
|
return data
|
||||||
|
}
|
106
src/routes/guestbook/+page.svelte
Normal file
106
src/routes/guestbook/+page.svelte
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Window from '../../components/window.svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
const hasPreviousPage = data.page > 1;
|
||||||
|
const hasNextPage = data.hasNext;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-row flex-wrap">
|
||||||
|
<div class="fixed">
|
||||||
|
<Window title="guestbook">
|
||||||
|
{@const ratelimited = data.ratelimitedFeat === 'send'}
|
||||||
|
<div class="flex flex-col gap-4 w-[60ch]">
|
||||||
|
<p>hia, here is the guestbook if you wanna post anything :)</p>
|
||||||
|
<p>just be a good human bean pretty please</p>
|
||||||
|
<form action={data.guestbook_url} method="post">
|
||||||
|
<div class="entry">
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<p class="place-self-start grow text-2xl font-monospace">###</p>
|
||||||
|
<p class="justify-end self-center text-sm font-monospace">...</p>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
class="text-lg ml-0.5 bg-inherit resize-none text-shadow-white placeholder-shown:[text-shadow:none] [field-sizing:content]"
|
||||||
|
name="content"
|
||||||
|
placeholder="say meow!"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p class="place-self-end text-sm font-monospace">
|
||||||
|
--- posted by <input
|
||||||
|
type="text"
|
||||||
|
name="author"
|
||||||
|
placeholder="author"
|
||||||
|
class="p-0 bg-inherit border-hidden max-w-[16ch] text-right text-sm text-shadow-white placeholder-shown:[text-shadow:none] [field-sizing:content]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4 mt-4">
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
value="post"
|
||||||
|
class="text-xl text-ralsei-green-light leading-5 motion-safe:hover:animate-bounce w-fit border-double border-4 p-1 pb-2"
|
||||||
|
/>
|
||||||
|
{#if ratelimited}
|
||||||
|
<p class="text-error self-center">you are ratelimited, try again in 30 seconds</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Window>
|
||||||
|
</div>
|
||||||
|
<div class="grow" />
|
||||||
|
<Window title="entries">
|
||||||
|
<div class="flex flex-col gap-4 w-[60ch] max-w-[60ch]">
|
||||||
|
{#if data.ratelimitedFeat === 'get'}
|
||||||
|
<p class="text-error">
|
||||||
|
woops, looks like you are being ratelimited, try again in like half a minute :3
|
||||||
|
</p>
|
||||||
|
{:else if data.fetchError}
|
||||||
|
<p class="text-error">got error trying to fetch entries, pls tell me about this</p>
|
||||||
|
<details>
|
||||||
|
<summary>error</summary>
|
||||||
|
<p>{data.fetchError}</p>
|
||||||
|
</details>
|
||||||
|
{:else}
|
||||||
|
{#each data.entries as [entry_id, entry] (entry_id)}
|
||||||
|
{@const date = new Date(entry.timestamp * 1e3).toLocaleString()}
|
||||||
|
<div class="entry">
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<p class="place-self-start grow text-2xl font-monospace">
|
||||||
|
#{entry_id}
|
||||||
|
</p>
|
||||||
|
<p class="justify-end self-center text-sm font-monospace">{date}</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg ml-0.5">{entry.content}</p>
|
||||||
|
<p class="place-self-end text-sm font-monospace">--- posted by {entry.author}</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p>looks like there are no entries :(</p>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{#if hasPreviousPage || hasNextPage}
|
||||||
|
<div class="flex flex-row w-full justify-center items-center font-monospace">
|
||||||
|
{#if hasPreviousPage}
|
||||||
|
<a href="/guestbook/?page={data.entries.length < 0 ? data.page - 1 : 1}"
|
||||||
|
><< previous</a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{#if hasNextPage && hasPreviousPage}
|
||||||
|
<div class="w-1/12" />
|
||||||
|
{/if}
|
||||||
|
{#if hasNextPage}
|
||||||
|
<a href="/guestbook/?page={data.page + 1}">next >></a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Window>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.entry {
|
||||||
|
@apply flex flex-col gap-3 py-2 px-3 bg-ralsei-green-dark/70 border-ralsei-green-light/30 border-x-[3px] border-y-4;
|
||||||
|
}
|
||||||
|
</style>
|
@ -35,10 +35,14 @@
|
|||||||
text-shadow: 0 0 3px theme(colors.ralsei.black), 0 0 6px theme(colors.ralsei.pink.neon), 0 0 10px #fff3;
|
text-shadow: 0 0 3px theme(colors.ralsei.black), 0 0 6px theme(colors.ralsei.pink.neon), 0 0 10px #fff3;
|
||||||
}
|
}
|
||||||
|
|
||||||
li,p {
|
li,p,summary,.text-shadow-white {
|
||||||
text-shadow: 0 0 1px theme(colors.ralsei.black), 0 0 5px theme(colors.ralsei.white);
|
text-shadow: 0 0 1px theme(colors.ralsei.black), 0 0 5px theme(colors.ralsei.white);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-shadow-red {
|
||||||
|
text-shadow: 0 0 1px theme(colors.ralsei.black), 0 0 5px theme(colors.red.600);
|
||||||
|
}
|
||||||
|
|
||||||
li::marker {
|
li::marker {
|
||||||
text-shadow: 0 0 4px theme(colors.ralsei.pink.regular), 0 0 6px #fff9;
|
text-shadow: 0 0 4px theme(colors.ralsei.pink.regular), 0 0 6px #fff9;
|
||||||
}
|
}
|
||||||
@ -47,7 +51,7 @@
|
|||||||
text-shadow: 0 0 4px theme(colors.ralsei.pink.regular);
|
text-shadow: 0 0 4px theme(colors.ralsei.pink.regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a,button,input[type=submit] {
|
||||||
text-shadow: 0 0 2px theme(colors.ralsei.black), 0 0 5px theme(colors.ralsei.green.light);
|
text-shadow: 0 0 2px theme(colors.ralsei.black), 0 0 5px theme(colors.ralsei.green.light);
|
||||||
cursor: url('/icons/gaze.png'), pointer;
|
cursor: url('/icons/gaze.png'), pointer;
|
||||||
}
|
}
|
||||||
@ -82,6 +86,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
.text-error {
|
||||||
|
@apply text-xl text-red-600 text-shadow-red;
|
||||||
|
}
|
||||||
|
|
||||||
.border-groove {
|
.border-groove {
|
||||||
border-style: groove;
|
border-style: groove;
|
||||||
}
|
}
|
||||||
|
BIN
static/icons/guestbook.png
Normal file
BIN
static/icons/guestbook.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 563 B |
@ -1,4 +1,4 @@
|
|||||||
import adapter from '@sveltejs/adapter-static';
|
import adapter from 'svelte-adapter-bun';
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
import { mdsvex } from 'mdsvex'
|
import { mdsvex } from 'mdsvex'
|
||||||
|
@ -52,7 +52,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require('@tailwindcss/typography')
|
require('@tailwindcss/typography'),
|
||||||
|
require('@tailwindcss/forms'),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user