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
|
||||
/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": {
|
||||
"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/kit": "^2.5.20",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.14",
|
||||
"@types/eslint": "^8.56.11",
|
||||
"@types/node": "^22.4.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.8.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) => {
|
||||
let sl = path.split('/')
|
||||
sl = sl.splice(1)
|
||||
if (sl.length > 1) {
|
||||
if (sl.length > 2) {
|
||||
sl[0] = sl[0][0]
|
||||
}
|
||||
const newPath = sl.join('/')
|
||||
|
@ -14,7 +14,8 @@
|
||||
let menuItems: MenuItem[] = [
|
||||
{ href: '', name: 'home', iconUri: '/icons/home.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('/');
|
||||
|
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;
|
||||
}
|
||||
|
||||
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-red {
|
||||
text-shadow: 0 0 1px theme(colors.ralsei.black), 0 0 5px theme(colors.red.600);
|
||||
}
|
||||
|
||||
li::marker {
|
||||
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);
|
||||
}
|
||||
|
||||
a {
|
||||
a,button,input[type=submit] {
|
||||
text-shadow: 0 0 2px theme(colors.ralsei.black), 0 0 5px theme(colors.ralsei.green.light);
|
||||
cursor: url('/icons/gaze.png'), pointer;
|
||||
}
|
||||
@ -82,6 +86,10 @@
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-error {
|
||||
@apply text-xl text-red-600 text-shadow-red;
|
||||
}
|
||||
|
||||
.border-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 { mdsvex } from 'mdsvex'
|
||||
|
@ -52,7 +52,8 @@ export default {
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/typography')
|
||||
require('@tailwindcss/typography'),
|
||||
require('@tailwindcss/forms'),
|
||||
],
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user