mirror of
https://github.com/httpjamesm/AnonymousOverflow.git
synced 2024-12-06 19:16:43 +01:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fae2717a35 | |||
| cf8892b4cb | |||
| c8a394e0ce | |||
| 6bfed5a9e3 | |||
| a0df61a92d | |||
| bbf098e7f4 | |||
| 2c4ded0800 | |||
| f0e42287cf | |||
| 9ebb6c90d9 | |||
| 69d96d646d | |||
| c87ce26da8 | |||
| 3dea27b09f | |||
| 4d8632e26c | |||
| eee8ec42c5 | |||
| d7bca93af8 | |||
| 2b608b4725 | |||
| bd48b309ac | |||
| 15b843d46b | |||
| 7b80c55dd2 | |||
| 12174b2600 | |||
| 5f11bcd6a2 | |||
| 7494b2df33 | |||
| 517a9afb89 | |||
| f9e42ca8c7 |
@@ -6,27 +6,33 @@ This project is super lightweight by design. The UI is simple and the frontend i
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## Clearnet Instances
|
||||
|
||||
| Instance URL | Region | Notes |
|
||||
| ---------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| [code.whatever.social](https://code.whatever.social) | United States & Germany | Operated by [Whatever Social](https://whatever.social) and [http.james](https://httpjames.space) |
|
||||
| [overflow.777.tf](https://overflow.777.tf/) | The Netherlands | Operated by [Jae](https://777.tf) |
|
||||
| [ao.vern.cc](https://ao.vern.cc) | United States | Operated by [vern.cc](https://vern.cc) |
|
||||
| [overflow.smnz.de](https://overflow.smnz.de) | Germany | Operated by [smnz.de](https://smnz.de) |
|
||||
| Instance URL | Region | Notes |
|
||||
| ------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| [code.whatever.social](https://code.whatever.social) | United States & Germany | Operated by [Whatever Social](https://whatever.social) and [http.james](https://httpjames.space) |
|
||||
| [overflow.777.tf](https://overflow.777.tf/) | The Netherlands | Operated by [Jae](https://777.tf) |
|
||||
| [ao.vern.cc](https://ao.vern.cc) | United States | Operated by [vern.cc](https://vern.cc) |
|
||||
| [overflow.smnz.de](https://overflow.smnz.de) | Germany | Operated by [smnz.de](https://smnz.de) |
|
||||
| [anonymousoverflow.esmailelbob.xyz](https://anonymousoverflow.esmailelbob.xyz/) | Canada | Operated by [Esmail EL BoB](https://esmailelbob.xyz) |
|
||||
| [overflow.lunar.icu](https://overflow.lunar.icu) | Germany | Operated by [lunar.icu](https://lunar.icu/) |
|
||||
| [overflow.adminforge.de](https://overflow.adminforge.de/) | Germany | Operated by [adminForge](https://adminforge.de/) |
|
||||
| [ao.foss.wtf](https://ao.foss.wtf) | Germany | Operated by [foss.wtf](https://foss.wtf) |
|
||||
| [overflow.hostux.net](https://overflow.hostux.net/) | France | Operated by [Hostux](https://hostux.net/) |
|
||||
|
||||
## Other Instances
|
||||
|
||||
| Instance URL | Region | Notes |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------------------------------------- |
|
||||
| [ao.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://ao.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | United States | Operated by [vern.cc](https://vern.cc) |
|
||||
| [vernmzgraj6aaoafmehupvtkkynpaa67rxcdj2kinwiy6konn6rq.b32.i2p](http://vernmzgraj6aaoafmehupvtkkynpaa67rxcdj2kinwiy6konn6rq.b32.i2p) | United States | Operated by [vern.cc](https://vern.cc) |
|
||||
| Instance URL | Region | Notes |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------- |
|
||||
| [ao.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://ao.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | United States | Operated by [vern.cc](https://vern.cc) |
|
||||
| [vernmzgraj6aaoafmehupvtkkynpaa67rxcdj2kinwiy6konn6rq.b32.i2p](http://vernmzgraj6aaoafmehupvtkkynpaa67rxcdj2kinwiy6konn6rq.b32.i2p) | United States | Operated by [vern.cc](https://vern.cc) |
|
||||
| [anonymousoverflow.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion](http://anonymousoverflow.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion) | Canada | Operated by [Esmail EL BoB](https://esmailelbob.xyz) |
|
||||
|
||||
## Why use AnonymousOverflow over StackOverflow?
|
||||
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
package config
|
||||
|
||||
var Version = "1.4"
|
||||
var Version = "1.6.1"
|
||||
|
||||
@@ -8,6 +8,7 @@ services:
|
||||
network: 'host'
|
||||
environment:
|
||||
- APP_URL=https://domain.com
|
||||
- JWT_SIGNING_SECRET=secret
|
||||
ports:
|
||||
- '80:8080'
|
||||
restart: 'always'
|
||||
Vendored
+1
@@ -10,6 +10,7 @@ import (
|
||||
func RunChecks() {
|
||||
godotenv.Load(".env")
|
||||
checkEnv("APP_URL")
|
||||
checkEnv("JWT_SIGNING_SECRET")
|
||||
}
|
||||
|
||||
func checkEnv(key string) {
|
||||
|
||||
@@ -17,6 +17,7 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.1 // indirect
|
||||
github.com/goccy/go-json v0.10.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
|
||||
github.com/joho/godotenv v1.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
|
||||
@@ -27,6 +27,8 @@ github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPr
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
||||
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
|
||||
@@ -32,7 +32,6 @@ func main() {
|
||||
r := gin.Default()
|
||||
|
||||
r.LoadHTMLGlob("templates/*")
|
||||
r.Static("/static", "./public")
|
||||
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(middleware.XssPreventionHeaders())
|
||||
@@ -40,6 +39,8 @@ func main() {
|
||||
r.Use(middleware.OptionsMiddleware())
|
||||
r.Use(middleware.Ratelimit())
|
||||
|
||||
r.GET("/static/*filepath", routes.StaticContent)
|
||||
|
||||
r.GET("/robots.txt", func(c *gin.Context) {
|
||||
c.String(200, "User-agent: *\nDisallow: /")
|
||||
})
|
||||
@@ -47,10 +48,15 @@ func main() {
|
||||
r.GET("/options/:name", routes.ChangeOptions)
|
||||
|
||||
r.GET("/", routes.GetHome)
|
||||
|
||||
r.POST("/", routes.PostHome)
|
||||
|
||||
r.GET("/questions/:id", func(c *gin.Context) {
|
||||
// redirect user to the question with the title
|
||||
c.Redirect(302, fmt.Sprintf("/questions/%s/placeholder", c.Param("id")))
|
||||
})
|
||||
r.GET("/questions/:id/:title", routes.ViewQuestion)
|
||||
|
||||
r.GET("/proxy", routes.GetImage)
|
||||
|
||||
r.Run(fmt.Sprintf("%s:%s", host, port))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 491 B |
+65
-8
@@ -29,6 +29,20 @@ body {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.card-tags .tag {
|
||||
background-color: var(--meta-bg);
|
||||
color: var(--tag-text-color);
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
font-size: 0.8rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--code-bg);
|
||||
padding: 0.15rem;
|
||||
@@ -45,27 +59,34 @@ pre {
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: var(--muted-text-color);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.post-divider,
|
||||
.answer-divider {
|
||||
.post-divider {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.answer-divider {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.answer {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--muted-text-color);
|
||||
}
|
||||
|
||||
.answer:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.answer-meta {
|
||||
border-radius: 5px;
|
||||
padding: 1rem;
|
||||
@@ -111,6 +132,42 @@ img {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.answers-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.answers-header .sorting form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.answers-header select {
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--meta-bg);
|
||||
color: var(--text-color);
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.answers-header button {
|
||||
width: 2rem;
|
||||
border: 0;
|
||||
height: 2rem;
|
||||
border-radius: 5px;
|
||||
background-color: var(--meta-bg);
|
||||
color: var(--text-color);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
body {
|
||||
padding-left: 1rem;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"anonymousoverflow/src/types"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
func GetImage(c *gin.Context) {
|
||||
|
||||
authorization := c.Query("auth")
|
||||
if authorization == "" {
|
||||
c.String(400, "Missing auth token")
|
||||
return
|
||||
}
|
||||
|
||||
// validate the auth token
|
||||
token, err := jwt.ParseWithClaims(authorization, &types.ImageProxyClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
|
||||
// validate the signing method
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(os.Getenv("JWT_SIGNING_SECRET")), nil
|
||||
})
|
||||
if err != nil {
|
||||
c.String(400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*types.ImageProxyClaims)
|
||||
if !ok || !token.Valid {
|
||||
c.String(400, "Invalid token")
|
||||
return
|
||||
}
|
||||
|
||||
if claims.Action != "imageProxy" {
|
||||
c.String(400, "Invalid action")
|
||||
return
|
||||
}
|
||||
|
||||
if claims.Exp < time.Now().Unix() {
|
||||
c.String(400, "Token expired")
|
||||
return
|
||||
}
|
||||
|
||||
// download the image
|
||||
client := resty.New()
|
||||
resp, err := client.R().Get(claims.ImageURL)
|
||||
if err != nil {
|
||||
c.AbortWithStatus(500)
|
||||
return
|
||||
}
|
||||
|
||||
// set the content type
|
||||
c.Header("Content-Type", resp.Header().Get("Content-Type"))
|
||||
|
||||
// write the image to the response
|
||||
c.Writer.Write(resp.Body())
|
||||
}
|
||||
+24
-3
@@ -21,6 +21,13 @@ import (
|
||||
var codeBlockRegex = regexp.MustCompile(`(?s)<pre><code>(.+?)<\/code><\/pre>`)
|
||||
var questionCodeBlockRegex = regexp.MustCompile(`(?s)<pre class=".+"><code( class=".+")?>(.+?)</code></pre>`)
|
||||
|
||||
var soSortValues = map[string]string{
|
||||
"votes": "scoredesc",
|
||||
"trending": "trending",
|
||||
"newest": "modifieddesc",
|
||||
"oldest": "createdasc",
|
||||
}
|
||||
|
||||
func ViewQuestion(c *gin.Context) {
|
||||
client := resty.New()
|
||||
|
||||
@@ -36,7 +43,17 @@ func ViewQuestion(c *gin.Context) {
|
||||
|
||||
questionTitle := c.Param("title")
|
||||
|
||||
soLink := fmt.Sprintf("https://stackoverflow.com/questions/%s/%s", questionId, questionTitle)
|
||||
sortValue := c.Query("sort_by")
|
||||
if sortValue == "" {
|
||||
sortValue = "votes"
|
||||
}
|
||||
|
||||
soSortValue, ok := soSortValues[sortValue]
|
||||
if !ok {
|
||||
soSortValue = soSortValues["votes"]
|
||||
}
|
||||
|
||||
soLink := fmt.Sprintf("https://stackoverflow.com/questions/%s/%s?answertab=%s", questionId, questionTitle, soSortValue)
|
||||
|
||||
resp, err := client.R().Get(soLink)
|
||||
if err != nil {
|
||||
@@ -81,6 +98,9 @@ func ViewQuestion(c *gin.Context) {
|
||||
|
||||
questionPostLayout := doc.Find("div.post-layout").First()
|
||||
|
||||
questionTags := utils.GetPostTags(questionPostLayout)
|
||||
newFilteredQuestion.Tags = questionTags
|
||||
|
||||
questionBodyParent := doc.Find("div.s-prose")
|
||||
|
||||
questionBodyParentHTML, err := questionBodyParent.Html()
|
||||
@@ -93,7 +113,7 @@ func ViewQuestion(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
newFilteredQuestion.Body = template.HTML(questionBodyParentHTML)
|
||||
newFilteredQuestion.Body = template.HTML(utils.ReplaceImgTags(questionBodyParentHTML))
|
||||
|
||||
questionBodyText := questionBodyParent.Text()
|
||||
|
||||
@@ -234,7 +254,7 @@ func ViewQuestion(c *gin.Context) {
|
||||
comments = utils.FindAndReturnComments(answerBodyHTML, postLayout)
|
||||
|
||||
newFilteredAnswer.Comments = comments
|
||||
newFilteredAnswer.Body = template.HTML(answerBodyHTML)
|
||||
newFilteredAnswer.Body = template.HTML(utils.ReplaceImgTags(answerBodyHTML))
|
||||
|
||||
answers = append(answers, newFilteredAnswer)
|
||||
})
|
||||
@@ -251,6 +271,7 @@ func ViewQuestion(c *gin.Context) {
|
||||
"imagePolicy": imagePolicy,
|
||||
"theme": c.MustGet("theme").(string),
|
||||
"currentUrl": fmt.Sprintf("%s/questions/%s/%s", os.Getenv("APP_URL"), questionId, questionTitle),
|
||||
"sortValue": sortValue,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func StaticContent(c *gin.Context) {
|
||||
cleanFilePath := strings.ReplaceAll(c.Param("filepath"), "..", "")
|
||||
|
||||
c.File(fmt.Sprintf("./public/%s", cleanFilePath))
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package types
|
||||
|
||||
import "github.com/golang-jwt/jwt/v4"
|
||||
|
||||
type ImageProxyClaims struct {
|
||||
Action string `json:"action"`
|
||||
|
||||
ImageURL string `json:"image_url"`
|
||||
|
||||
Iss int64 `json:"iss"`
|
||||
Exp int64 `json:"exp"`
|
||||
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
@@ -10,4 +10,5 @@ type FilteredQuestion struct {
|
||||
AuthorURL string
|
||||
ShortenedBody string
|
||||
Comments []FilteredComment
|
||||
Tags []string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"anonymousoverflow/src/types"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
var imgTagRegex = regexp.MustCompile(`<img[^>]*\s+src\s*=\s*"(.*?)"[^>]*>`)
|
||||
|
||||
func ReplaceImgTags(inHtml string) string {
|
||||
// find all img tags
|
||||
imgTags := imgTagRegex.FindAllString(inHtml, -1)
|
||||
|
||||
for _, imgTag := range imgTags {
|
||||
// parse the src="" attribute
|
||||
srcRegex := regexp.MustCompile(`src\s*=\s*"(.*?)"`)
|
||||
src := srcRegex.FindStringSubmatch(imgTag)[1]
|
||||
|
||||
authToken, _ := generateImageProxyAuth(src)
|
||||
|
||||
// replace the img tag with a proxied url
|
||||
inHtml = strings.Replace(inHtml, imgTag, fmt.Sprintf(`<img src="%s/proxy?auth=%s">`, os.Getenv("APP_URL"), authToken), 1)
|
||||
}
|
||||
|
||||
return inHtml
|
||||
}
|
||||
|
||||
func generateImageProxyAuth(url string) (string, error) {
|
||||
// generate a jwt with types.ImageProxyClaims
|
||||
claims := types.ImageProxyClaims{
|
||||
Action: "imageProxy",
|
||||
ImageURL: url,
|
||||
Iss: time.Now().Unix(),
|
||||
Exp: time.Now().Add(time.Minute).Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
|
||||
|
||||
// sign the token
|
||||
ss, err := token.SignedString([]byte(os.Getenv("JWT_SIGNING_SECRET")))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ss, nil
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func GetPostTags(postLayout *goquery.Selection) []string {
|
||||
var tags []string
|
||||
postLayout.Find("a.post-tag").Each(func(i int, s *goquery.Selection) {
|
||||
tags = append(tags, s.Text())
|
||||
})
|
||||
return tags
|
||||
}
|
||||
+28
-8
@@ -38,13 +38,35 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-body">{{ .question.Body }}</div>
|
||||
{{ if .question.Comments }}
|
||||
{{ template "comments.html" .question }}
|
||||
{{end}}
|
||||
|
||||
<div class="card-tags">
|
||||
{{ range .question.Tags }}
|
||||
<div class="tag">{{ . }}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ if .question.Comments }} {{ template "comments.html"
|
||||
.question }} {{end}}
|
||||
</div>
|
||||
<hr class="post-divider" />
|
||||
<h2>Answers</h2>
|
||||
<div class="answers-header">
|
||||
<h2>Answers</h2>
|
||||
<div class="sorting">
|
||||
<form>
|
||||
<select name="sort_by">
|
||||
<option disabled value="">Sort answers by...</option>
|
||||
<option value="votes"{{ if eq .sortValue "votes" }} selected{{ end }}>Votes</option>
|
||||
<option value="trending"{{ if eq .sortValue "trending" }} selected{{ end }}>Trending</option>
|
||||
<option value="newest"{{ if eq .sortValue "newest" }} selected{{ end }}>Date modified (newest first)</option>
|
||||
<option value="oldest"{{ if eq .sortValue "oldest" }} selected{{ end }}>Date created (oldest first)</option>
|
||||
</select>
|
||||
<button type="submit">
|
||||
<img
|
||||
src="/static/icons/sort.svg"
|
||||
alt="Sieve icon"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{ range $answer := .answers }}
|
||||
<div class="answer">
|
||||
<div
|
||||
@@ -65,11 +87,9 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{{ if $answer.Comments }}
|
||||
{{ template "comments.html" $answer }}
|
||||
{{ if $answer.Comments }} {{ template "comments.html" $answer }}
|
||||
{{end}}
|
||||
</div>
|
||||
<hr class="answer-divider" />
|
||||
{{ end }}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user