mirror of
https://github.com/yanislav-igonin/micrach
synced 2024-12-22 14:22:33 +03:00
Feature - Migrator (#3)
* feat: rename init migration * feat: add migration for is_archived field * fix: thread title wrap * fix: captcha checks after post validation now * fix * wip on migrate method * feat: decided to write own migrator * feat: add get files on folder func * wip * feat: add migrations table * feat: add migration method * doc * feat: query -> exec
This commit is contained in:
parent
c1f632ee03
commit
242abb8645
@ -101,17 +101,6 @@ func CreateThread(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
captchaID := form.Value["captchaId"][0]
|
|
||||||
captchaString := form.Value["captcha"][0]
|
|
||||||
isCaptchaValid := captcha.VerifyString(captchaID, captchaString)
|
|
||||||
if !isCaptchaValid {
|
|
||||||
errorHtmlData := Repositories.BadRequestHtmlData{
|
|
||||||
Message: Repositories.InvalidCaptchaErrorMessage,
|
|
||||||
}
|
|
||||||
c.HTML(http.StatusInternalServerError, "400.html", errorHtmlData)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: dat shit crashes if no fields in request
|
// TODO: dat shit crashes if no fields in request
|
||||||
text := form.Value["text"][0]
|
text := form.Value["text"][0]
|
||||||
title := form.Value["title"][0]
|
title := form.Value["title"][0]
|
||||||
@ -125,6 +114,17 @@ func CreateThread(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
captchaID := form.Value["captchaId"][0]
|
||||||
|
captchaString := form.Value["captcha"][0]
|
||||||
|
isCaptchaValid := captcha.VerifyString(captchaID, captchaString)
|
||||||
|
if !isCaptchaValid {
|
||||||
|
errorHtmlData := Repositories.BadRequestHtmlData{
|
||||||
|
Message: Repositories.InvalidCaptchaErrorMessage,
|
||||||
|
}
|
||||||
|
c.HTML(http.StatusInternalServerError, "400.html", errorHtmlData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
conn, err := Db.Pool.Acquire(context.TODO())
|
conn, err := Db.Pool.Acquire(context.TODO())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("error:", err)
|
log.Println("error:", err)
|
||||||
@ -225,17 +225,6 @@ func UpdateThread(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
captchaID := form.Value["captchaId"][0]
|
|
||||||
captchaString := form.Value["captcha"][0]
|
|
||||||
isCaptchaValid := captcha.VerifyString(captchaID, captchaString)
|
|
||||||
if !isCaptchaValid {
|
|
||||||
errorHtmlData := Repositories.BadRequestHtmlData{
|
|
||||||
Message: Repositories.InvalidCaptchaErrorMessage,
|
|
||||||
}
|
|
||||||
c.HTML(http.StatusInternalServerError, "400.html", errorHtmlData)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: dat shit crashes if no fields in request
|
// TODO: dat shit crashes if no fields in request
|
||||||
text := form.Value["text"][0]
|
text := form.Value["text"][0]
|
||||||
filesInRequest := form.File["files"]
|
filesInRequest := form.File["files"]
|
||||||
@ -248,6 +237,17 @@ func UpdateThread(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
captchaID := form.Value["captchaId"][0]
|
||||||
|
captchaString := form.Value["captcha"][0]
|
||||||
|
isCaptchaValid := captcha.VerifyString(captchaID, captchaString)
|
||||||
|
if !isCaptchaValid {
|
||||||
|
errorHtmlData := Repositories.BadRequestHtmlData{
|
||||||
|
Message: Repositories.InvalidCaptchaErrorMessage,
|
||||||
|
}
|
||||||
|
c.HTML(http.StatusInternalServerError, "400.html", errorHtmlData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
isSageField := form.Value["sage"]
|
isSageField := form.Value["sage"]
|
||||||
var isSageString string
|
var isSageString string
|
||||||
if len(isSageField) != 0 {
|
if len(isSageField) != 0 {
|
||||||
|
67
db/db.go
67
db/db.go
@ -3,14 +3,24 @@ package db
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
Config "micrach/config"
|
Config "micrach/config"
|
||||||
|
Files "micrach/files"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Pool *pgxpool.Pool
|
var Pool *pgxpool.Pool
|
||||||
|
|
||||||
|
type MigrationsMap map[int]string
|
||||||
|
type Migration struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var err error
|
var err error
|
||||||
Pool, err = pgxpool.Connect(context.TODO(), Config.Db.Url)
|
Pool, err = pgxpool.Connect(context.TODO(), Config.Db.Url)
|
||||||
@ -21,3 +31,60 @@ func Init() {
|
|||||||
|
|
||||||
log.Println("database - online")
|
log.Println("database - online")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Migrate() {
|
||||||
|
dbMigrations := getDbMigrations()
|
||||||
|
sqlMigrations := Files.GetFullFilePathsInFolder("migrations")
|
||||||
|
for _, m := range sqlMigrations {
|
||||||
|
filename := filepath.Base(m)
|
||||||
|
splitted := strings.Split(filename, "-")
|
||||||
|
id, err := strconv.Atoi(splitted[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
// Get name without extension
|
||||||
|
name := strings.Split(splitted[1], ".")[0]
|
||||||
|
|
||||||
|
_, isMigrationInDb := dbMigrations[id]
|
||||||
|
if !isMigrationInDb {
|
||||||
|
_, err := Pool.Exec(context.TODO(), Files.ReadFileText(m))
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := `INSERT INTO migrations (id, name) VALUES ($1, $2)`
|
||||||
|
_, err = Pool.Query(context.TODO(), sql, id, name)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
log.Println("database migration - " + name + " - online")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("database migrations - online")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDbMigrations() MigrationsMap {
|
||||||
|
sql := `SELECT id, name FROM migrations`
|
||||||
|
rows, err := Pool.Query(context.TODO(), sql)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows.Err() != nil {
|
||||||
|
log.Panicln(rows.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationsMap := make(MigrationsMap)
|
||||||
|
for rows.Next() {
|
||||||
|
var m Migration
|
||||||
|
err = rows.Scan(&m.ID, &m.Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationsMap[m.ID] = m.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrationsMap
|
||||||
|
}
|
||||||
|
40
files/get_files_in_folder.go
Normal file
40
files/get_files_in_folder.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reads folder and returns full file paths slice
|
||||||
|
func GetFullFilePathsInFolder(folder string) []string {
|
||||||
|
currentPath, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullFolderPath := path.Join(currentPath, folder)
|
||||||
|
files, err := ioutil.ReadDir(fullFolderPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var paths []string
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
paths = append(paths, path.Join(fullFolderPath, file.Name()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads file contents by full path and returns string
|
||||||
|
func ReadFileText(fullFilePath string) string {
|
||||||
|
file, err := ioutil.ReadFile(fullFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(file)
|
||||||
|
}
|
4
go.mod
4
go.mod
@ -7,18 +7,18 @@ require (
|
|||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/gin-gonic/gin v1.7.4
|
github.com/gin-gonic/gin v1.7.4
|
||||||
github.com/go-playground/validator/v10 v10.9.0 // indirect
|
github.com/go-playground/validator/v10 v10.9.0 // indirect
|
||||||
github.com/golang-migrate/migrate v3.5.4+incompatible // indirect
|
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/jackc/pgtype v1.8.1
|
github.com/jackc/pgtype v1.8.1
|
||||||
github.com/jackc/pgx/v4 v4.13.0
|
github.com/jackc/pgx/v4 v4.13.0
|
||||||
github.com/jackc/puddle v1.1.4 // indirect
|
github.com/jackc/puddle v1.1.4 // indirect
|
||||||
github.com/joho/godotenv v1.3.0
|
github.com/joho/godotenv v1.3.0
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/lib/pq v1.10.3 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/ugorji/go v1.2.6 // indirect
|
github.com/ugorji/go v1.2.6 // indirect
|
||||||
github.com/ulule/limiter/v3 v3.8.0
|
github.com/ulule/limiter/v3 v3.8.0
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect
|
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
google.golang.org/protobuf v1.27.1 // indirect
|
google.golang.org/protobuf v1.27.1 // indirect
|
||||||
|
9
go.sum
9
go.sum
@ -42,8 +42,6 @@ github.com/go-redis/redis/v8 v8.4.2/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hb
|
|||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA=
|
|
||||||
github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
@ -137,8 +135,9 @@ github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ic
|
|||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
|
||||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
|
||||||
|
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
@ -221,8 +220,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||||
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/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
1
main.go
1
main.go
@ -22,6 +22,7 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
Config.Init()
|
Config.Init()
|
||||||
Db.Init()
|
Db.Init()
|
||||||
|
Db.Migrate()
|
||||||
defer Db.Pool.Close()
|
defer Db.Pool.Close()
|
||||||
gin.SetMode(Config.App.Env)
|
gin.SetMode(Config.App.Env)
|
||||||
if Config.App.SeedDb {
|
if Config.App.SeedDb {
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
DROP TABLE files;
|
|
||||||
DROP TABLE posts;
|
|
@ -1,5 +1,3 @@
|
|||||||
-- UP
|
|
||||||
-- Posts
|
|
||||||
CREATE TABLE posts
|
CREATE TABLE posts
|
||||||
(
|
(
|
||||||
id SERIAL NOT NULL,
|
id SERIAL NOT NULL,
|
||||||
@ -19,8 +17,6 @@ CREATE TABLE posts
|
|||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
-- Files
|
|
||||||
CREATE TABLE files
|
CREATE TABLE files
|
||||||
(
|
(
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
@ -31,3 +27,10 @@ CREATE TABLE files
|
|||||||
created_at TIMESTAMP DEFAULT NOW() NOT NULL,
|
created_at TIMESTAMP DEFAULT NOW() NOT NULL,
|
||||||
FOREIGN KEY (post_id) REFERENCES posts (id)
|
FOREIGN KEY (post_id) REFERENCES posts (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE migrations
|
||||||
|
(
|
||||||
|
id INT NOT NULL,
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW() NOT NULL
|
||||||
|
)
|
2
migrations/2-posts_archivation.sql
Normal file
2
migrations/2-posts_archivation.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE posts
|
||||||
|
ADD COLUMN is_archived BOOLEAN DEFAULT false;
|
@ -4,3 +4,7 @@
|
|||||||
.thread-title-link:hover {
|
.thread-title-link:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thread-title {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<a href="/" class="thread-title-link">
|
<a href="/" class="thread-title-link">
|
||||||
<h1 class="display-1 text-center">
|
<h1 class="display-1 text-center thread-title">
|
||||||
⤶
|
⤶
|
||||||
{{ if ne $FirstPost.Title "" }}
|
{{ if ne $FirstPost.Title "" }}
|
||||||
{{$FirstPost.Title}}
|
{{$FirstPost.Title}}
|
||||||
|
Loading…
Reference in New Issue
Block a user