Compare commits
14
Commits
7415beda73
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c780e5b8f | ||
|
|
1055495e1d | ||
|
|
28c643943f | ||
|
|
e2605dcd54 | ||
|
|
6dad00d41d | ||
|
|
fa37b2c302 | ||
|
|
9026b43c83 | ||
|
|
58b51e6e36 | ||
|
|
16678c4e7a | ||
|
|
6f3561a7ac | ||
|
|
9202017284 | ||
|
|
04a3a4c5d4 | ||
|
|
276f84b6ff | ||
|
|
a176ac0663 |
+1
-1
@@ -3,7 +3,7 @@ internal/database/comfychan.db
|
||||
tmp
|
||||
out
|
||||
|
||||
media/
|
||||
/media/
|
||||
|
||||
# direnv
|
||||
.direnv
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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:
|
||||
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.bin "true" \
|
||||
--build.full_bin "true" \
|
||||
--build.delay "100" \
|
||||
--build.exclude_dir "" \
|
||||
--build.include_dir "web/static" \
|
||||
@@ -17,21 +17,8 @@ db/seed:
|
||||
sqlite3 ./internal/database/comfychan.db < ./internal/database/seed.sql
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@@ -1 +1,40 @@
|
||||
# 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).
|
||||
|
||||
@@ -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"
|
||||
@@ -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/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
||||
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/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/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
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/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/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/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.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/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -20,7 +20,7 @@ type Queryer interface {
|
||||
|
||||
func GetBoards(db *sql.DB) ([]Board, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT id, name, slug, tag
|
||||
SELECT id, name, slug, tag
|
||||
FROM boards ORDER BY slug`)
|
||||
|
||||
if err != nil {
|
||||
@@ -43,8 +43,8 @@ func GetBoards(db *sql.DB) ([]Board, error) {
|
||||
|
||||
func GetBoard(db *sql.DB, slug string) (Board, error) {
|
||||
row := db.QueryRow(`
|
||||
SELECT id, name, slug, tag
|
||||
FROM boards
|
||||
SELECT id, name, slug, tag
|
||||
FROM boards
|
||||
WHERE slug = ?`, slug)
|
||||
|
||||
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) {
|
||||
rows, err := db.Query(`
|
||||
SELECT id, board_slug, subject, created_at, bumped_at, pinned, locked
|
||||
FROM threads
|
||||
SELECT id, board_slug, subject, created_at, bumped_at, pinned, locked
|
||||
FROM threads
|
||||
WHERE board_slug = ?`, boardSlug)
|
||||
|
||||
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) {
|
||||
row := db.QueryRow(`
|
||||
SELECT id, board_slug, subject, created_at, bumped_at, pinned, locked
|
||||
FROM threads
|
||||
SELECT id, board_slug, subject, created_at, bumped_at, pinned, locked
|
||||
FROM threads
|
||||
WHERE id = ?`, threadId)
|
||||
|
||||
var t Thread
|
||||
@@ -212,9 +212,9 @@ func DeleteThread(db Queryer, threadId int) error {
|
||||
|
||||
func GetPosts(db *sql.DB, threadId int) ([]Post, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT id, thread_id, author, body, created_at, media_path,
|
||||
ip_hash, number, thumb_path, banned
|
||||
FROM posts
|
||||
SELECT id, thread_id, author, body, created_at, media_path,
|
||||
ip_hash, number, thumb_path, banned
|
||||
FROM posts
|
||||
WHERE thread_id = ?`, threadId)
|
||||
|
||||
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) {
|
||||
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
|
||||
FROM posts
|
||||
WHERE thread_id = ?
|
||||
FROM posts
|
||||
WHERE thread_id = ?
|
||||
ORDER BY created_at ASC LIMIT 1`, threadId)
|
||||
|
||||
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) {
|
||||
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
|
||||
FROM posts
|
||||
FROM posts
|
||||
WHERE id = ?`, postId)
|
||||
|
||||
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 {
|
||||
row := db.QueryRow(`
|
||||
SELECT MAX(p.number)
|
||||
FROM posts p
|
||||
FROM posts p
|
||||
INNER JOIN threads t ON p.thread_id = t.id
|
||||
WHERE t.board_slug = ?`, boardSlug)
|
||||
|
||||
@@ -290,7 +290,7 @@ func PutPost(db Queryer, boardSlug string, threadId int, body string, mediaPath
|
||||
}
|
||||
|
||||
_, 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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -337,7 +337,7 @@ func DeletePost(db *sql.DB, postId int) error {
|
||||
|
||||
// delete post
|
||||
_, err := db.Exec(`
|
||||
DELETE FROM posts
|
||||
DELETE FROM posts
|
||||
WHERE id = ?`, postId)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -364,7 +364,7 @@ var ErrBanNotFound = errors.New("ban not found")
|
||||
|
||||
func GetBan(db *sql.DB, ip string) (Ban, error) {
|
||||
row := db.QueryRow(`
|
||||
SELECT ip_hash, reason, expiration
|
||||
SELECT ip_hash, reason, expiration
|
||||
FROM bans
|
||||
where ip_hash = ?`, ip)
|
||||
|
||||
@@ -398,7 +398,7 @@ func GetAdmin(db *sql.DB, username string) (Admin, error) {
|
||||
|
||||
var result Admin
|
||||
if err := row.Scan(&result.Username, &result.Password); err != nil {
|
||||
return Admin{}, nil
|
||||
return Admin{}, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
@@ -11,8 +11,8 @@ CREATE TABLE IF NOT EXISTS boards (
|
||||
tag TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS threads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
CREATE TABLE IF NOT EXISTS threads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
board_slug TEXT NOT NULL,
|
||||
subject TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -46,7 +46,7 @@ CREATE TABLE IF NOT EXISTS bans (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip_hash TEXT NOT NULL UNIQUE,
|
||||
reason TEXT NOT NULL,
|
||||
expiration DATETIME NOT NULL
|
||||
expiration DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- ======================
|
||||
@@ -55,10 +55,10 @@ CREATE TABLE IF NOT EXISTS bans (
|
||||
|
||||
-- Boards
|
||||
|
||||
INSERT INTO boards (slug, name, tag) VALUES
|
||||
('c', 'Comfy', 'Be comfy, fren'),
|
||||
INSERT INTO boards (slug, name, tag) VALUES
|
||||
('c', 'Comfy', 'Stay awhile'),
|
||||
('r', 'Robots', 'Beep, boop'),
|
||||
('gn', 'Goon', 'God is watching');
|
||||
|
||||
INSERT INTO admins (username, password) VALUES
|
||||
('admin', '$2a$10$vRP4/9O6SwyUziEUtBLQM.r9C2WujIIZ6yEgqGjhlBaFPvtpfdHPC');
|
||||
('admin', '$2a$10$hzmcLK2ZrEz0NTxr7eVuV.gn8shW.tQxD0D0vYUgAwADZly3U/BZ.');
|
||||
|
||||
+9
-13
@@ -49,27 +49,23 @@ var urlRx = regexp.MustCompile(`(?i)\bhttps?://[^\s<]+`)
|
||||
func EnrichPost(body string) string {
|
||||
var b strings.Builder
|
||||
|
||||
for _, rawLine := range strings.Split(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,
|
||||
)
|
||||
})
|
||||
|
||||
for rawLine := range strings.SplitSeq(body, "\n") {
|
||||
var outLine string
|
||||
for i, rawWord := range strings.Split(line, " ") {
|
||||
for i, rawWord := range strings.Split(rawLine, " ") {
|
||||
var outWord string
|
||||
if strings.HasPrefix(rawWord, ">>") {
|
||||
postId := strings.TrimPrefix(rawWord, ">>")
|
||||
if postId, ok := strings.CutPrefix(rawWord, ">>"); ok {
|
||||
outWord = fmt.Sprintf(
|
||||
`<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>`,
|
||||
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 {
|
||||
outWord = rawWord
|
||||
outWord = template.HTMLEscapeString(rawWord)
|
||||
}
|
||||
if i != 0 {
|
||||
outLine += " "
|
||||
|
||||
@@ -71,9 +71,6 @@ func main() {
|
||||
// init paths
|
||||
|
||||
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_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
@@ -2,27 +2,28 @@
|
||||
:root {
|
||||
--bg-main: #1d1f21;
|
||||
--text-color: #c5c8c6;
|
||||
--text-muted: #81a2be;
|
||||
--brand-red: #c5c8c6;
|
||||
--text-muted: #85a28a;
|
||||
--brand-green: #4e5f3f;
|
||||
--brand-yellow: #db9f12;
|
||||
|
||||
--form-header-bg: #282a2e;
|
||||
--border-light: #282a2e;
|
||||
--subject: #b294bb;
|
||||
--link-secondary: #81a2be;
|
||||
--subject: #718a5b;
|
||||
--link-secondary: #85a28a;
|
||||
|
||||
--post-bg: #282a2e;
|
||||
--post-highlight: #D6BAD0;
|
||||
--post-author: #c5c8c6;
|
||||
--greentext: #b5bd68;
|
||||
--greentext: #949055;
|
||||
|
||||
--reply-link: #81a2be;
|
||||
--reply-link: #85a28a;
|
||||
--dialog-bg: #EDEFF7;
|
||||
|
||||
--danger: #b294bb;
|
||||
--warning-bg: #b294bb;
|
||||
--danger: #4e5f3f;
|
||||
--warning-bg: #929661;
|
||||
--black: #000000;
|
||||
|
||||
--heading-bg: #b294bb45;
|
||||
--heading-bg: #4e5f3f;
|
||||
--heading-text: #FFFFFF;
|
||||
--box-bg: #282a2e;
|
||||
|
||||
@@ -41,6 +42,15 @@ body {
|
||||
color: var(--text-color)
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* INDEX */
|
||||
|
||||
#clavis {
|
||||
@@ -52,25 +62,16 @@ body {
|
||||
/* BOARD LIST */
|
||||
|
||||
#boardList {
|
||||
font-size: 12pt;
|
||||
cursor: default;
|
||||
font-size: 14pt;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#boardList a {
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
#boardList a:hover {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* BOARD */
|
||||
|
||||
.board-header {
|
||||
margin: 25px auto;
|
||||
text-align: center;
|
||||
color: var(--brand-red);
|
||||
}
|
||||
|
||||
.board-header h1 {
|
||||
@@ -78,15 +79,19 @@ body {
|
||||
font-size: 1.8rem;
|
||||
margin: 8px;
|
||||
margin-top: 12px;
|
||||
color: var(--brand-yellow);
|
||||
}
|
||||
|
||||
.board-header p {
|
||||
font-size: .8rem;
|
||||
margin: 4px;
|
||||
font-style: italic;
|
||||
color: var(--brand-yellow);
|
||||
}
|
||||
|
||||
.board-header img {
|
||||
width: 300px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
@@ -422,11 +427,29 @@ body {
|
||||
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;
|
||||
margin: 0;
|
||||
background-color: var(--heading-bg);
|
||||
color: var(--heading-text);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.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
@@ -4,16 +4,40 @@ import "github.com/dominicf2001/comfychan/web/views/shared"
|
||||
|
||||
templ Index() {
|
||||
@shared.Layout("Comfychan") {
|
||||
<img style="width: 150px; float: right;" src="/static/media/comfychan.png"/>
|
||||
<section class="container">
|
||||
<img id="clavis" src="/static/media/clavis.png"/>
|
||||
<div class="box">
|
||||
<h2>Welcome to Comfychan</h2>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user