14 Commits
Author SHA1 Message Date
dominicf 6c780e5b8f Update /c/ banner 2026-08-24 13:05:49 -04:00
dominicf 1055495e1d Update index page 2026-08-24 12:12:03 -04:00
dominicf 28c643943f Styling/color enhancements 2026-08-24 11:33:19 -04:00
dominicf e2605dcd54 Styles for table and global links 2026-08-24 11:13:38 -04:00
dominicf 6dad00d41d Fix hot reload proxy address 2026-08-24 10:48:29 -04:00
dominicf fa37b2c302 Run and apply go fix 2026-08-24 10:40:23 -04:00
dominicf 9026b43c83 Update admin 2026-08-23 18:19:22 -04:00
dominicf 58b51e6e36 Fix err not propogating in GetAdmin 2026-08-23 18:09:11 -04:00
dominicf 16678c4e7a Patch XSS vuln 2026-08-23 18:06:29 -04:00
dominicf 6f3561a7ac Update favicon to comfychan 2026-08-23 17:21:33 -04:00
dominicf 9202017284 Remove unneeded fallback for dataDir 2026-08-22 21:53:48 -04:00
dominicf 04a3a4c5d4 Write README 2026-08-22 21:50:49 -04:00
dominicf 276f84b6ff Update makefile 2026-08-22 21:49:52 -04:00
dominicf a176ac0663 Add deploy.sh script 2026-08-22 21:29:32 -04:00
15 changed files with 177 additions and 94 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ internal/database/comfychan.db
tmp tmp
out out
media/ /media/
# direnv # direnv
.direnv .direnv
+5 -18
View File
@@ -1,10 +1,10 @@
live/templ: live/templ:
templ generate --watch --proxy="http://localhost:8080" --cmd="go run ./web" --open-browser=false -v templ generate --watch --proxy="http://localhost:7676" --cmd="go run ./web" --open-browser=false -v
live/sync_assets: live/sync_assets:
go run github.com/air-verse/air@v1.61.7 \ go run github.com/air-verse/air@latest \
--build.cmd "templ generate --notify-proxy" \ --build.cmd "templ generate --notify-proxy" \
--build.bin "true" \ --build.full_bin "true" \
--build.delay "100" \ --build.delay "100" \
--build.exclude_dir "" \ --build.exclude_dir "" \
--build.include_dir "web/static" \ --build.include_dir "web/static" \
@@ -17,21 +17,8 @@ db/seed:
sqlite3 ./internal/database/comfychan.db < ./internal/database/seed.sql sqlite3 ./internal/database/comfychan.db < ./internal/database/seed.sql
db/seed/f: db/seed/f:
rm ./internal/database/comfychan.db && sqlite3 ./internal/database/comfychan.db < ./internal/database/seed.sql rm ./internal/database/comfychan.db && make db/seed
build:
build:
templ generate templ generate
go build -o ./out/comfychan ./web go build -o ./out/comfychan ./web
deploy:
sudo mkdir -p /var/lib/comfychan/web/static
sudo mkdir -p /var/lib/comfychan/internal/database
sudo cp ./out/comfychan /srv/comfychan/
sudo cp -r web/static/* /var/lib/comfychan/web/static/
db/deploy:
sudo mkdir -p /var/lib/comfychan/internal/database
sudo sqlite3 /var/lib/comfychan/internal/database/comfychan.db < ./internal/database/seed.sql
sudo chown comfychan:comfychan /var/lib/private/comfychan/internal/database/comfychan.db
+39
View File
@@ -1 +1,40 @@
# comfychan # comfychan
A small imageboard server written in Go, using [chi](https://github.com/go-chi/chi) for routing, [templ](https://templ.guide) for HTML templating, [htmx](https://htmx.org) for interactivity, and SQLite for storage.
## Requirements
- Go 1.25+
- [templ CLI](https://templ.guide/quick-start/installation) (`go install github.com/a-h/templ/cmd/templ@latest`), matching the version in `go.mod`
- `sqlite3` (for seeding the database)
## Development
```
make live
```
Starts the app with live reload: `templ` watches and regenerates `.templ` files, and static assets (`web/static`) trigger a browser refresh via proxy on `http://localhost:8080`.
Seed the database:
```
make db/seed # seed
make db/seed/f # wipe and reseed
```
## Building
```
make build
```
Generates templates and builds a binary at `./out/comfychan`.
## Configuration
- `COMFYCHAN_DATA_DIR` — base directory for runtime data (SQLite database, uploaded media, static assets). Defaults to the current directory if unset.
## Deployment
`./deploy.sh` builds the binary and ships it to a self-hosted server running comfychan as a systemd service (see script for target host/paths).
Executable
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
HOST="root@mediaserver"
BIN_DST="/srv/comfychan/comfychan"
DATA_DIR="/var/lib/comfychan"
SERVICE="comfychan"
echo "==> Building"
templ generate
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o ./out/comfychan ./web
echo "==> Ensuring remote directories exist"
ssh "$HOST" "mkdir -p $DATA_DIR/web/static $DATA_DIR/internal/database"
echo "==> Uploading binary"
# Upload beside the live binary, then atomically rename over it.
scp ./out/comfychan "$HOST:${BIN_DST}.new"
ssh "$HOST" "mv ${BIN_DST}.new $BIN_DST"
echo "==> Uploading static assets"
scp -r web/static/. "$HOST:$DATA_DIR/web/static/"
echo "==> Restarting $SERVICE"
ssh "$HOST" "systemctl restart $SERVICE && systemctl --no-pager status $SERVICE"
echo "==> Done"
-10
View File
@@ -1,26 +1,16 @@
github.com/a-h/templ v0.3.1001 h1:yHDTgexACdJttyiyamcTHXr2QkIeVF1MukLy44EAhMY=
github.com/a-h/templ v0.3.1001/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw= github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM= github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY= github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.50 h1:dmdFvo1XG4MPzA4IkAmE9upVz/Nj31uRoM5+jC8hYbY= github.com/mattn/go-sqlite3 v1.14.50 h1:dmdFvo1XG4MPzA4IkAmE9upVz/Nj31uRoM5+jC8hYbY=
github.com/mattn/go-sqlite3 v1.14.50/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/mattn/go-sqlite3 v1.14.50/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0= golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4= golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+20 -20
View File
@@ -20,7 +20,7 @@ type Queryer interface {
func GetBoards(db *sql.DB) ([]Board, error) { func GetBoards(db *sql.DB) ([]Board, error) {
rows, err := db.Query(` rows, err := db.Query(`
SELECT id, name, slug, tag SELECT id, name, slug, tag
FROM boards ORDER BY slug`) FROM boards ORDER BY slug`)
if err != nil { if err != nil {
@@ -43,8 +43,8 @@ func GetBoards(db *sql.DB) ([]Board, error) {
func GetBoard(db *sql.DB, slug string) (Board, error) { func GetBoard(db *sql.DB, slug string) (Board, error) {
row := db.QueryRow(` row := db.QueryRow(`
SELECT id, name, slug, tag SELECT id, name, slug, tag
FROM boards FROM boards
WHERE slug = ?`, slug) WHERE slug = ?`, slug)
var result Board var result Board
@@ -58,8 +58,8 @@ func GetBoard(db *sql.DB, slug string) (Board, error) {
func GetThreads(db *sql.DB, boardSlug string) ([]Thread, error) { func GetThreads(db *sql.DB, boardSlug string) ([]Thread, error) {
rows, err := db.Query(` rows, err := db.Query(`
SELECT id, board_slug, subject, created_at, bumped_at, pinned, locked SELECT id, board_slug, subject, created_at, bumped_at, pinned, locked
FROM threads FROM threads
WHERE board_slug = ?`, boardSlug) WHERE board_slug = ?`, boardSlug)
if err != nil { if err != nil {
@@ -84,8 +84,8 @@ func GetThreads(db *sql.DB, boardSlug string) ([]Thread, error) {
func GetThread(db *sql.DB, threadId int) (Thread, error) { func GetThread(db *sql.DB, threadId int) (Thread, error) {
row := db.QueryRow(` row := db.QueryRow(`
SELECT id, board_slug, subject, created_at, bumped_at, pinned, locked SELECT id, board_slug, subject, created_at, bumped_at, pinned, locked
FROM threads FROM threads
WHERE id = ?`, threadId) WHERE id = ?`, threadId)
var t Thread var t Thread
@@ -212,9 +212,9 @@ func DeleteThread(db Queryer, threadId int) error {
func GetPosts(db *sql.DB, threadId int) ([]Post, error) { func GetPosts(db *sql.DB, threadId int) ([]Post, error) {
rows, err := db.Query(` rows, err := db.Query(`
SELECT id, thread_id, author, body, created_at, media_path, SELECT id, thread_id, author, body, created_at, media_path,
ip_hash, number, thumb_path, banned ip_hash, number, thumb_path, banned
FROM posts FROM posts
WHERE thread_id = ?`, threadId) WHERE thread_id = ?`, threadId)
if err != nil { if err != nil {
@@ -239,10 +239,10 @@ func GetPosts(db *sql.DB, threadId int) ([]Post, error) {
func GetOriginalPost(db *sql.DB, threadId int) (Post, error) { func GetOriginalPost(db *sql.DB, threadId int) (Post, error) {
row := db.QueryRow(` row := db.QueryRow(`
SELECT id, thread_id, author, body, created_at, media_path, SELECT id, thread_id, author, body, created_at, media_path,
ip_hash, number, thumb_path, banned ip_hash, number, thumb_path, banned
FROM posts FROM posts
WHERE thread_id = ? WHERE thread_id = ?
ORDER BY created_at ASC LIMIT 1`, threadId) ORDER BY created_at ASC LIMIT 1`, threadId)
var r Post var r Post
@@ -257,9 +257,9 @@ func GetOriginalPost(db *sql.DB, threadId int) (Post, error) {
func GetPost(db *sql.DB, postId int) (Post, error) { func GetPost(db *sql.DB, postId int) (Post, error) {
row := db.QueryRow(` row := db.QueryRow(`
SELECT id, thread_id, author, body, created_at, media_path, SELECT id, thread_id, author, body, created_at, media_path,
ip_hash, number, thumb_path, banned ip_hash, number, thumb_path, banned
FROM posts FROM posts
WHERE id = ?`, postId) WHERE id = ?`, postId)
var r Post var r Post
@@ -275,7 +275,7 @@ func GetPost(db *sql.DB, postId int) (Post, error) {
func PutPost(db Queryer, boardSlug string, threadId int, body string, mediaPath string, thumbPath string, ip_hash string) error { func PutPost(db Queryer, boardSlug string, threadId int, body string, mediaPath string, thumbPath string, ip_hash string) error {
row := db.QueryRow(` row := db.QueryRow(`
SELECT MAX(p.number) SELECT MAX(p.number)
FROM posts p FROM posts p
INNER JOIN threads t ON p.thread_id = t.id INNER JOIN threads t ON p.thread_id = t.id
WHERE t.board_slug = ?`, boardSlug) WHERE t.board_slug = ?`, boardSlug)
@@ -290,7 +290,7 @@ func PutPost(db Queryer, boardSlug string, threadId int, body string, mediaPath
} }
_, err := db.Exec(` _, err := db.Exec(`
INSERT INTO posts (thread_id, body, media_path, ip_hash, number, thumb_path) INSERT INTO posts (thread_id, body, media_path, ip_hash, number, thumb_path)
VALUES (?, ?, ?, ?, ?, ?)`, threadId, body, mediaPath, ip_hash, newPostNumber, thumbPath) VALUES (?, ?, ?, ?, ?, ?)`, threadId, body, mediaPath, ip_hash, newPostNumber, thumbPath)
if err != nil { if err != nil {
return err return err
@@ -337,7 +337,7 @@ func DeletePost(db *sql.DB, postId int) error {
// delete post // delete post
_, err := db.Exec(` _, err := db.Exec(`
DELETE FROM posts DELETE FROM posts
WHERE id = ?`, postId) WHERE id = ?`, postId)
if err != nil { if err != nil {
return err return err
@@ -364,7 +364,7 @@ var ErrBanNotFound = errors.New("ban not found")
func GetBan(db *sql.DB, ip string) (Ban, error) { func GetBan(db *sql.DB, ip string) (Ban, error) {
row := db.QueryRow(` row := db.QueryRow(`
SELECT ip_hash, reason, expiration SELECT ip_hash, reason, expiration
FROM bans FROM bans
where ip_hash = ?`, ip) where ip_hash = ?`, ip)
@@ -398,7 +398,7 @@ func GetAdmin(db *sql.DB, username string) (Admin, error) {
var result Admin var result Admin
if err := row.Scan(&result.Username, &result.Password); err != nil { if err := row.Scan(&result.Username, &result.Password); err != nil {
return Admin{}, nil return Admin{}, err
} }
return result, nil return result, nil
+6 -6
View File
@@ -11,8 +11,8 @@ CREATE TABLE IF NOT EXISTS boards (
tag TEXT NOT NULL tag TEXT NOT NULL
); );
CREATE TABLE IF NOT EXISTS threads ( CREATE TABLE IF NOT EXISTS threads (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
board_slug TEXT NOT NULL, board_slug TEXT NOT NULL,
subject TEXT NOT NULL DEFAULT '', subject TEXT NOT NULL DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -46,7 +46,7 @@ CREATE TABLE IF NOT EXISTS bans (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_hash TEXT NOT NULL UNIQUE, ip_hash TEXT NOT NULL UNIQUE,
reason TEXT NOT NULL, reason TEXT NOT NULL,
expiration DATETIME NOT NULL expiration DATETIME NOT NULL
); );
-- ====================== -- ======================
@@ -55,10 +55,10 @@ CREATE TABLE IF NOT EXISTS bans (
-- Boards -- Boards
INSERT INTO boards (slug, name, tag) VALUES INSERT INTO boards (slug, name, tag) VALUES
('c', 'Comfy', 'Be comfy, fren'), ('c', 'Comfy', 'Stay awhile'),
('r', 'Robots', 'Beep, boop'), ('r', 'Robots', 'Beep, boop'),
('gn', 'Goon', 'God is watching'); ('gn', 'Goon', 'God is watching');
INSERT INTO admins (username, password) VALUES INSERT INTO admins (username, password) VALUES
('admin', '$2a$10$vRP4/9O6SwyUziEUtBLQM.r9C2WujIIZ6yEgqGjhlBaFPvtpfdHPC'); ('admin', '$2a$10$hzmcLK2ZrEz0NTxr7eVuV.gn8shW.tQxD0D0vYUgAwADZly3U/BZ.');
+9 -13
View File
@@ -49,27 +49,23 @@ var urlRx = regexp.MustCompile(`(?i)\bhttps?://[^\s<]+`)
func EnrichPost(body string) string { func EnrichPost(body string) string {
var b strings.Builder var b strings.Builder
for _, rawLine := range strings.Split(body, "\n") { for rawLine := range strings.SplitSeq(body, "\n") {
line := urlRx.ReplaceAllStringFunc(rawLine, func(u string) string {
esc := template.HTMLEscapeString(u)
return fmt.Sprintf(
`<a href="%[1]s" target="_blank" rel="noopener noreferrer" class="ext-link">%[1]s</a>`,
esc,
)
})
var outLine string var outLine string
for i, rawWord := range strings.Split(line, " ") { for i, rawWord := range strings.Split(rawLine, " ") {
var outWord string var outWord string
if strings.HasPrefix(rawWord, ">>") { if postId, ok := strings.CutPrefix(rawWord, ">>"); ok {
postId := strings.TrimPrefix(rawWord, ">>")
outWord = fmt.Sprintf( outWord = fmt.Sprintf(
`<a onclick="onReplyLinkClick(event)" onmouseover="highlightPost(%[1]s,event)" `+ `<a onclick="onReplyLinkClick(event)" onmouseover="highlightPost(%[1]s,event)" `+
`onmouseleave="highlightPost(%[1]s,event,false)" href="#post-%[1]s" class="reply-link">%[2]s</a>`, `onmouseleave="highlightPost(%[1]s,event,false)" href="#post-%[1]s" class="reply-link">%[2]s</a>`,
postId, template.HTMLEscapeString(rawWord), postId, template.HTMLEscapeString(rawWord),
) )
} else if urlRx.MatchString(rawWord) {
outWord = fmt.Sprintf(
`<a href="%[1]s" target="_blank" rel="noopener noreferrer" class="ext-link">%[1]s</a>`,
template.HTMLEscapeString(rawWord),
)
} else { } else {
outWord = rawWord outWord = template.HTMLEscapeString(rawWord)
} }
if i != 0 { if i != 0 {
outLine += " " outLine += " "
-3
View File
@@ -71,9 +71,6 @@ func main() {
// init paths // init paths
dataDir := os.Getenv("COMFYCHAN_DATA_DIR") dataDir := os.Getenv("COMFYCHAN_DATA_DIR")
if dataDir == "" {
dataDir = "." // for development
}
util.POST_MEDIA_FULL_PATH = filepath.Join(dataDir, util.POST_MEDIA_FULL_PATH) util.POST_MEDIA_FULL_PATH = filepath.Join(dataDir, util.POST_MEDIA_FULL_PATH)
util.POST_MEDIA_THUMB_PATH = filepath.Join(dataDir, util.POST_MEDIA_THUMB_PATH) util.POST_MEDIA_THUMB_PATH = filepath.Join(dataDir, util.POST_MEDIA_THUMB_PATH)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

+44 -21
View File
@@ -2,27 +2,28 @@
:root { :root {
--bg-main: #1d1f21; --bg-main: #1d1f21;
--text-color: #c5c8c6; --text-color: #c5c8c6;
--text-muted: #81a2be; --text-muted: #85a28a;
--brand-red: #c5c8c6; --brand-green: #4e5f3f;
--brand-yellow: #db9f12;
--form-header-bg: #282a2e; --form-header-bg: #282a2e;
--border-light: #282a2e; --border-light: #282a2e;
--subject: #b294bb; --subject: #718a5b;
--link-secondary: #81a2be; --link-secondary: #85a28a;
--post-bg: #282a2e; --post-bg: #282a2e;
--post-highlight: #D6BAD0; --post-highlight: #D6BAD0;
--post-author: #c5c8c6; --post-author: #c5c8c6;
--greentext: #b5bd68; --greentext: #949055;
--reply-link: #81a2be; --reply-link: #85a28a;
--dialog-bg: #EDEFF7; --dialog-bg: #EDEFF7;
--danger: #b294bb; --danger: #4e5f3f;
--warning-bg: #b294bb; --warning-bg: #929661;
--black: #000000; --black: #000000;
--heading-bg: #b294bb45; --heading-bg: #4e5f3f;
--heading-text: #FFFFFF; --heading-text: #FFFFFF;
--box-bg: #282a2e; --box-bg: #282a2e;
@@ -41,6 +42,15 @@ body {
color: var(--text-color) color: var(--text-color)
} }
a {
cursor: pointer;
color: var(--text-muted);
}
a:hover {
color: var(--danger);
}
/* INDEX */ /* INDEX */
#clavis { #clavis {
@@ -52,25 +62,16 @@ body {
/* BOARD LIST */ /* BOARD LIST */
#boardList { #boardList {
font-size: 12pt; cursor: default;
font-size: 14pt;
margin: 10px; margin: 10px;
} }
#boardList a {
cursor: pointer;
color: var(--text-muted);
}
#boardList a:hover {
color: var(--danger);
}
/* BOARD */ /* BOARD */
.board-header { .board-header {
margin: 25px auto; margin: 25px auto;
text-align: center; text-align: center;
color: var(--brand-red);
} }
.board-header h1 { .board-header h1 {
@@ -78,15 +79,19 @@ body {
font-size: 1.8rem; font-size: 1.8rem;
margin: 8px; margin: 8px;
margin-top: 12px; margin-top: 12px;
color: var(--brand-yellow);
} }
.board-header p { .board-header p {
font-size: .8rem; font-size: .8rem;
margin: 4px; margin: 4px;
font-style: italic;
color: var(--brand-yellow);
} }
.board-header img { .board-header img {
width: 300px; width: 300px;
height: 100px;
} }
.action-bar { .action-bar {
@@ -422,11 +427,29 @@ body {
background-color: var(--box-bg); background-color: var(--box-bg);
} }
.box h2 { .box-table {
font-size: 10pt;
border: 1px solid var(--black);
background-color: var(--box-bg);
width: 100%;
text-align: center;
}
.box-table td {
padding: 5px;
}
.box-table tbody tr:nth-child(even) {
background-color: color-mix(in srgb, var(--box-bg) 85%, black);
}
.box h2,
.box-table th {
padding: 5px; padding: 5px;
margin: 0; margin: 0;
background-color: var(--heading-bg); background-color: var(--heading-bg);
color: var(--heading-text); color: var(--heading-text);
font-weight: bold;
} }
.box p { .box p {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+26 -2
View File
@@ -4,16 +4,40 @@ import "github.com/dominicf2001/comfychan/web/views/shared"
templ Index() { templ Index() {
@shared.Layout("Comfychan") { @shared.Layout("Comfychan") {
<img style="width: 150px; float: right;" src="/static/media/comfychan.png"/>
<section class="container"> <section class="container">
<img id="clavis" src="/static/media/clavis.png"/>
<div class="box"> <div class="box">
<h2>Welcome to Comfychan</h2> <h2>Welcome to Comfychan</h2>
<div> <div>
<p> <p>
<strong>Comfychan</strong> is place to share comfy images and videos. <strong>Comfychan</strong> is place to share comfy images, videos and retardation.
</p> </p>
</div> </div>
</div> </div>
</section> </section>
<section style="margin-top: 25px;" class="container">
<table class="box-table">
<thead>
<tr>
<th>Board</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="/c">/c/ - Comfy</a></td>
<td>Stay awhile</td>
</tr>
<tr>
<td><a href="/r">/r/ - Robots</a></td>
<td>Beep, boop</td>
</tr>
<tr>
<td><a href="/gn">/gn/ - Goon</a></td>
<td>God is watching</td>
</tr>
</tbody>
</table>
</section>
} }
} }