Compare commits
No commits in common. "master" and "project-index-rework" have entirely different histories.
master
...
project-in
|
@ -16,7 +16,6 @@ local/backups
|
|||
/tmp
|
||||
*.exe
|
||||
.DS_Store
|
||||
__debug_bin*
|
||||
|
||||
# vim session saves
|
||||
Session.vim
|
||||
|
|
2
go.mod
|
@ -11,7 +11,6 @@ require (
|
|||
github.com/aws/aws-sdk-go-v2/credentials v1.3.3
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.13.0
|
||||
github.com/aws/smithy-go v1.7.0
|
||||
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f
|
||||
github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8
|
||||
github.com/go-stack/stack v1.8.0
|
||||
github.com/google/uuid v1.2.0
|
||||
|
@ -57,7 +56,6 @@ require (
|
|||
github.com/mitchellh/reflectwalk v1.0.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/teambition/rrule-go v1.7.2 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
golang.org/x/net v0.6.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 // indirect
|
||||
|
|
4
go.sum
|
@ -88,8 +88,6 @@ github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
|
|||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f h1:feGUUxxvOtWVOhTko8Cbmp33a+tU0IMZxMEmnkoAISQ=
|
||||
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
|
@ -277,8 +275,6 @@ github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vA
|
|||
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300/go.mod h1:FNa/dfN95vAYCNFrIKRrlRo+MBLbwmR9Asa5f2ljmBI=
|
||||
github.com/teacat/noire v1.1.0 h1:5IgJ1H8jodiSSYnrVadV2JjbAnEgCCjYUQxSUuaQ7Sg=
|
||||
github.com/teacat/noire v1.1.0/go.mod h1:cetGlnqr+9yKJcFgRgYXOWJY66XIrrjUsGBwNlNNtAk=
|
||||
github.com/teambition/rrule-go v1.7.2 h1:goEajFWYydfCgavn2m/3w5U+1b3PGqPUHx/fFSVfTy0=
|
||||
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/wellington/go-libsass v0.9.2 h1:6Ims04UDdBs6/CGSVK5JC8FNikR5ssrsMMKE/uaO5Q8=
|
||||
github.com/wellington/go-libsass v0.9.2/go.mod h1:mxgxgam0N0E+NAUMHLcu20Ccfc3mVpDkyrLDayqfiTs=
|
||||
|
|
Before Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 228 KiB |
Before Width: | Height: | Size: 267 KiB |
|
@ -1 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 236 253" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="books" x="0" y="0" width="235.74" height="252.97" style="fill:none;"/><path d="M77.61,252.704l0,-230.569l80.52,-0l-0,230.569l-80.52,0Zm59.686,-188.853l0,-20.883l-38.853,-0l0,20.883l38.853,0Zm0,128.926l0,-108.092l-38.853,-0l0,108.092l38.853,-0Zm-38.853,20.833l0,18.261l38.853,-0l0,-18.261l-38.853,0Zm-98.032,39.094l-0,-252.438l70.429,-0l-0,252.438l-70.429,0Zm49.596,-206.372l-0,-25.233l-28.763,-0l0,25.233l28.763,0Zm-0,142.369l-0,-121.536l-28.763,0l0,121.536l28.763,-0Zm-28.763,20.833l0,22.337c0,-0 28.763,-0 28.763,-0l-0,-22.337l-28.763,-0Zm143.656,43.17l-0,-252.438l70.429,-0l-0,252.438l-70.429,0Zm49.596,-206.372l-0,-25.233l-28.763,-0l0,25.233l28.763,0Zm-0,142.369l-0,-121.536l-28.763,0l0,121.536l28.763,-0Zm-28.763,20.833l0,22.337l28.763,-0l-0,-22.337l-28.763,-0Z"/></svg>
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.8 KiB |
|
@ -1 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 336 233" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="lightbulb" x="0" y="0" width="335.159" height="232.656" style="fill:none;"/><path d="M197.613,202.015c0,16.576 -13.457,30.034 -30.034,30.034c-16.576,-0 -30.034,-13.458 -30.034,-30.034l60.068,-0Zm0.06,-9.97l-60.206,-0c-3.155,-16.04 -5.152,-23.793 -32.39,-57.373c-10.513,-12.96 -16.178,-29.812 -16.178,-47.773c0,-43.425 35.256,-78.68 78.68,-78.68c43.425,-0 78.68,34.8 78.68,78.224c0,17.961 -5.665,34.814 -16.177,47.774c-27.238,33.58 -29.254,41.788 -32.409,57.828Zm-16.343,-20.834c3.88,-11.276 11.569,-24.225 32.572,-50.118c7.611,-9.383 11.524,-21.647 11.524,-34.65c-0,-31.893 -25.954,-57.391 -57.847,-57.391c-31.926,0 -57.846,25.92 -57.846,57.847c-0,13.003 3.913,25.266 11.524,34.649c20.946,25.823 28.639,38.556 32.521,49.663l27.552,0Zm-160.921,-151.684c-4.694,-3.321 -5.808,-9.827 -2.488,-14.52c3.321,-4.693 9.827,-5.808 14.52,-2.488l31.239,22.101c4.694,3.321 5.808,9.827 2.488,14.52c-3.32,4.693 -9.827,5.808 -14.52,2.488l-31.239,-22.101Zm12.032,153.97c-4.693,3.32 -11.199,2.205 -14.52,-2.488c-3.32,-4.693 -2.206,-11.199 2.488,-14.52l31.239,-22.101c4.693,-3.32 11.2,-2.205 14.52,2.488c3.32,4.693 2.206,11.199 -2.488,14.52l-31.239,22.101Zm-21.856,-74.77c-5.749,-0 -10.416,-4.668 -10.416,-10.417c-0,-5.749 4.667,-10.417 10.416,-10.417l38.267,0c5.749,0 10.417,4.668 10.417,10.417c-0,5.749 -4.668,10.417 -10.417,10.417l-38.267,-0Zm292.133,-95.906c4.693,-3.32 11.199,-2.205 14.519,2.488c3.321,4.693 2.206,11.199 -2.487,14.52l-31.239,22.101c-4.694,3.32 -11.2,2.206 -14.52,-2.488c-3.321,-4.693 -2.206,-11.199 2.487,-14.52l31.24,-22.101Zm12.032,153.97c4.693,3.321 5.808,9.827 2.487,14.52c-3.32,4.694 -9.826,5.808 -14.519,2.488l-31.24,-22.101c-4.693,-3.321 -5.808,-9.827 -2.487,-14.52c3.32,-4.693 9.826,-5.808 14.52,-2.488l31.239,22.101Zm9.823,-78.595c5.75,-0 10.417,4.667 10.417,10.416c0,5.749 -4.667,10.417 -10.417,10.417l-38.266,-0c-5.75,-0 -10.417,-4.668 -10.417,-10.417c-0,-5.749 4.667,-10.416 10.417,-10.416l38.266,-0Z"/></svg>
|
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 12 KiB |
|
@ -1 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 301 229" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="presentation" x="-0" y="0" width="300.716" height="228.578" style="fill:none;"/><path d="M124.979,14.95l175.159,0l0,152.429l-137.896,0c-1.49,-7.262 -3.898,-14.19 -7.097,-20.659l124.334,-0l-0,-111.11l-143.514,-0c-2.302,-7.632 -6.074,-14.628 -10.986,-20.66Zm-119.77,196.191c-2.997,-8.252 -4.631,-17.155 -4.631,-26.437c0,-42.797 34.745,-77.542 77.541,-77.542c42.796,0 77.541,34.745 77.541,77.542c0,9.282 -1.634,18.185 -4.631,26.437c-22.091,10.548 -46.816,16.455 -72.91,16.455c-26.094,0 -50.819,-5.907 -72.91,-16.455Zm15.522,-13.612c17.693,7.296 37.075,11.317 57.388,11.317c20.314,0 39.695,-4.021 57.389,-11.317c0.919,-4.129 1.402,-8.421 1.402,-12.825c0,-32.448 -26.343,-58.792 -58.791,-58.792c-32.448,0 -58.791,26.344 -58.791,58.792c0,4.404 0.484,8.696 1.403,12.825Zm57.388,-196.547c28.741,-0 52.075,23.334 52.075,52.074c-0,28.741 -23.334,52.075 -52.075,52.075c-28.74,-0 -52.074,-23.334 -52.074,-52.075c-0,-28.74 23.334,-52.074 52.074,-52.074Zm0,18.75c-18.392,-0 -33.324,14.932 -33.324,33.324c-0,18.392 14.932,33.325 33.324,33.325c18.392,-0 33.325,-14.933 33.325,-33.325c-0,-18.392 -14.933,-33.324 -33.325,-33.324Z"/></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -7346,6 +7346,12 @@ article code {
|
|||
margin-left: auto;
|
||||
margin-right: auto; }
|
||||
|
||||
.flex-shrink-0, .edit-form .edit-form-row > :first-child {
|
||||
flex-shrink: 0; }
|
||||
|
||||
.flex-grow-1, .edit-form .edit-form-row > :nth-child(2) {
|
||||
flex-grow: 1; }
|
||||
|
||||
.flex-fair {
|
||||
flex-basis: 1px;
|
||||
flex-grow: 1;
|
||||
|
@ -7591,19 +7597,7 @@ article code {
|
|||
grid-template-columns: 1fr 1fr; }
|
||||
.bg--dim-ns {
|
||||
background-color: #f0f0f0;
|
||||
background-color: var(--dim-background); }
|
||||
.g0-ns {
|
||||
gap: 0; }
|
||||
.g1-ns {
|
||||
gap: 0.25rem; }
|
||||
.g2-ns {
|
||||
gap: 0.5rem; }
|
||||
.g3-ns {
|
||||
gap: 1rem; }
|
||||
.g4-ns {
|
||||
gap: 2rem; }
|
||||
.g5-ns {
|
||||
gap: 4rem; } }
|
||||
background-color: var(--dim-background); } }
|
||||
|
||||
@media screen and (min-width: 35em) and (max-width: 60em) {
|
||||
.bi-avoid-m {
|
||||
|
@ -7632,19 +7626,7 @@ article code {
|
|||
grid-template-columns: 1fr 1fr; }
|
||||
.bg--dim-m {
|
||||
background-color: #f0f0f0;
|
||||
background-color: var(--dim-background); }
|
||||
.g0-m {
|
||||
gap: 0; }
|
||||
.g1-m {
|
||||
gap: 0.25rem; }
|
||||
.g2-m {
|
||||
gap: 0.5rem; }
|
||||
.g3-m {
|
||||
gap: 1rem; }
|
||||
.g4-m {
|
||||
gap: 2rem; }
|
||||
.g5-m {
|
||||
gap: 4rem; } }
|
||||
background-color: var(--dim-background); } }
|
||||
|
||||
@media screen and (min-width: 60em) {
|
||||
.bi-avoid-l {
|
||||
|
@ -7673,19 +7655,7 @@ article code {
|
|||
grid-template-columns: 1fr 1fr; }
|
||||
.bg--dim-l {
|
||||
background-color: #f0f0f0;
|
||||
background-color: var(--dim-background); }
|
||||
.g0-l {
|
||||
gap: 0; }
|
||||
.g1-l {
|
||||
gap: 0.25rem; }
|
||||
.g2-l {
|
||||
gap: 0.5rem; }
|
||||
.g3-l {
|
||||
gap: 1rem; }
|
||||
.g4-l {
|
||||
gap: 2rem; }
|
||||
.g5-l {
|
||||
gap: 4rem; } }
|
||||
background-color: var(--dim-background); } }
|
||||
|
||||
.not-first:first-child {
|
||||
display: none; }
|
||||
|
|
|
@ -1,383 +0,0 @@
|
|||
package calendar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/config"
|
||||
"git.handmade.network/hmn/hmn/src/jobs"
|
||||
"git.handmade.network/hmn/hmn/src/logging"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
"github.com/emersion/go-ical"
|
||||
)
|
||||
|
||||
type RawCalendarData struct {
|
||||
Name string
|
||||
Url string
|
||||
Data []byte
|
||||
Hash [sha1.Size]byte
|
||||
}
|
||||
|
||||
type CalendarEvent struct {
|
||||
ID string
|
||||
Name string
|
||||
Desc string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Duration time.Duration
|
||||
CalName string
|
||||
}
|
||||
|
||||
var unifiedCalendar *ical.Calendar
|
||||
var rawCalendarData = make([]*RawCalendarData, 0)
|
||||
var cachedICals = make(map[string][]byte)
|
||||
var httpClient = http.Client{}
|
||||
|
||||
// NOTE(asaf): Passing an empty array for selectedCals returns all cals
|
||||
func GetICal(selectedCals []string) ([]byte, error) {
|
||||
if unifiedCalendar == nil {
|
||||
return nil, oops.New(nil, "No calendar")
|
||||
}
|
||||
sort.Strings(selectedCals)
|
||||
cacheKey := strings.Join(selectedCals, "##")
|
||||
cachedICal, ok := cachedICals[cacheKey]
|
||||
if ok {
|
||||
return cachedICal, nil
|
||||
}
|
||||
|
||||
var cal *ical.Calendar
|
||||
if len(selectedCals) == 0 {
|
||||
cal = unifiedCalendar
|
||||
} else {
|
||||
cal = newHMNCalendar()
|
||||
for _, child := range unifiedCalendar.Children {
|
||||
include := true
|
||||
|
||||
if child.Name == ical.CompEvent {
|
||||
calName, _ := child.Props.Text(ical.PropComment)
|
||||
if calName != "" {
|
||||
found := false
|
||||
for _, s := range selectedCals {
|
||||
if calName == s {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
include = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if include {
|
||||
cal.Children = append(cal.Children, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
var calBytes []byte
|
||||
if len(cal.Children) > 0 {
|
||||
var buffer bytes.Buffer
|
||||
err := ical.NewEncoder(&buffer).Encode(cal)
|
||||
if err != nil {
|
||||
return nil, oops.New(err, "Failed to encode calendar to iCal")
|
||||
}
|
||||
calBytes = buffer.Bytes()
|
||||
} else {
|
||||
calBytes = emptyCalendarString()
|
||||
}
|
||||
cachedICals[cacheKey] = calBytes
|
||||
return calBytes, nil
|
||||
}
|
||||
|
||||
func GetFutureEvents() []CalendarEvent {
|
||||
if unifiedCalendar == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
futureEvents := make([]CalendarEvent, 0)
|
||||
eventObjects := unifiedCalendar.Events()
|
||||
now := time.Now()
|
||||
lastTime := now.Add(time.Hour * 24 * 365)
|
||||
for _, ev := range eventObjects {
|
||||
summary, err := ev.Props.Text(ical.PropSummary)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).Msg("Failed to get summary for calendar event")
|
||||
continue
|
||||
}
|
||||
|
||||
startTime, err := ev.DateTimeStart(nil)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).Str("Event name", summary).Msg("Failed to get start time for calendar event")
|
||||
continue
|
||||
}
|
||||
|
||||
var evTimes []time.Time
|
||||
set, err := ev.RecurrenceSet(nil)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).Str("Event name", summary).Msg("Failed to get recurrence set for calendar event")
|
||||
continue
|
||||
}
|
||||
if set != nil {
|
||||
evTimes = set.Between(now, lastTime, true)
|
||||
} else if startTime.After(now) {
|
||||
evTimes = []time.Time{startTime}
|
||||
}
|
||||
|
||||
if len(evTimes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
desc, err := ev.Props.Text(ical.PropDescription)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).Str("Event name", summary).Msg("Failed to get description for calendar event")
|
||||
continue
|
||||
}
|
||||
|
||||
calName, _ := ev.Props.Text(ical.PropComment)
|
||||
|
||||
uid, err := ev.Props.Text(ical.PropUID)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).Str("Event name", summary).Msg("Failed to get uid for calendar event")
|
||||
continue
|
||||
}
|
||||
|
||||
endTime, err := ev.DateTimeStart(nil)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).Str("Event name", summary).Msg("Failed to get end time for calendar event")
|
||||
continue
|
||||
}
|
||||
|
||||
evDuration := endTime.Sub(startTime)
|
||||
|
||||
for _, t := range evTimes {
|
||||
futureEvents = append(futureEvents, CalendarEvent{
|
||||
ID: uid,
|
||||
Name: summary,
|
||||
Desc: desc,
|
||||
StartTime: t,
|
||||
EndTime: t.Add(evDuration),
|
||||
Duration: evDuration,
|
||||
CalName: calName,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(futureEvents, func(i, j int) bool {
|
||||
return futureEvents[i].StartTime.Before(futureEvents[j].StartTime)
|
||||
})
|
||||
return futureEvents
|
||||
}
|
||||
|
||||
func MonitorCalendars(ctx context.Context) jobs.Job {
|
||||
log := logging.ExtractLogger(ctx).With().Str("calendar goroutine", "calendar monitor").Logger()
|
||||
if len(config.Config.Calendars) == 0 {
|
||||
log.Info().Msg("No calendars specified in config")
|
||||
return jobs.Noop()
|
||||
}
|
||||
|
||||
ctx = logging.AttachLoggerToContext(&log, ctx)
|
||||
|
||||
job := jobs.New()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
log.Info().Msg("Shutting down calendar monitor")
|
||||
job.Done()
|
||||
}()
|
||||
log.Info().Msg("Running calendar monitor")
|
||||
|
||||
monitorTimer := time.NewTimer(time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-monitorTimer.C:
|
||||
err := func() (err error) {
|
||||
defer utils.RecoverPanicAsError(&err)
|
||||
|
||||
ReloadCalendars(ctx)
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
logging.Error().Err(err).Msg("Panicked in MonitorCalendars")
|
||||
}
|
||||
monitorTimer.Reset(time.Minute)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
func ReloadCalendars(ctx context.Context) {
|
||||
log := logging.ExtractLogger(ctx)
|
||||
|
||||
// Download calendars
|
||||
calChan := make(chan RawCalendarData, len(config.Config.Calendars))
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(config.Config.Calendars))
|
||||
for _, c := range config.Config.Calendars {
|
||||
go func(cal config.CalendarSource) {
|
||||
defer func() {
|
||||
wg.Done()
|
||||
logging.LogPanics(log)
|
||||
}()
|
||||
calUrl := cal.Url
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", calUrl, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create request for calendar fetch")
|
||||
return
|
||||
}
|
||||
res, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("Url", calUrl).Msg("Failed to fetch calendar")
|
||||
return
|
||||
}
|
||||
if res.StatusCode > 299 || !strings.HasPrefix(res.Header.Get("Content-Type"), "text/calendar") {
|
||||
log.Error().Str("Url", calUrl).Str("Status", res.Status).Msg("Failed to fetch calendar")
|
||||
io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("Url", calUrl).Msg("Failed to fetch calendar")
|
||||
return
|
||||
}
|
||||
|
||||
calChan <- RawCalendarData{Name: cal.Name, Url: calUrl, Data: data}
|
||||
}(c)
|
||||
}
|
||||
wg.Wait()
|
||||
newRawCalendarData := make([]*RawCalendarData, 0, len(config.Config.Calendars))
|
||||
Collect:
|
||||
for {
|
||||
select {
|
||||
case d := <-calChan:
|
||||
newRawCalendarData = append(newRawCalendarData, &d)
|
||||
default:
|
||||
break Collect
|
||||
}
|
||||
}
|
||||
|
||||
// Diff calendars
|
||||
same := true
|
||||
for _, n := range newRawCalendarData {
|
||||
n.Hash = sha1.Sum(n.Data)
|
||||
}
|
||||
|
||||
sort.Slice(newRawCalendarData, func(i, j int) bool {
|
||||
return newRawCalendarData[i].Name < newRawCalendarData[j].Name
|
||||
})
|
||||
|
||||
if len(newRawCalendarData) != len(rawCalendarData) {
|
||||
same = false
|
||||
} else {
|
||||
for i := range newRawCalendarData {
|
||||
newData := newRawCalendarData[i]
|
||||
oldData := rawCalendarData[i]
|
||||
if newData.Name != oldData.Name {
|
||||
same = false
|
||||
break
|
||||
}
|
||||
if newData.Hash != oldData.Hash {
|
||||
same = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if same {
|
||||
return
|
||||
}
|
||||
|
||||
// Unify calendars and clear cache
|
||||
rawCalendarData = newRawCalendarData
|
||||
cachedICals = make(map[string][]byte)
|
||||
|
||||
unified := newHMNCalendar()
|
||||
|
||||
var timezones []string
|
||||
|
||||
for _, calData := range rawCalendarData {
|
||||
decoder := ical.NewDecoder(bytes.NewReader(calData.Data))
|
||||
calNameProp := ical.NewProp(ical.PropComment)
|
||||
calNameProp.SetText(calData.Name)
|
||||
for {
|
||||
cal, err := decoder.Decode()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
log.Error().Err(err).Str("Url", calData.Url).Msg("Failed to parse calendar")
|
||||
break
|
||||
}
|
||||
|
||||
for _, child := range cal.Children {
|
||||
if child.Name == ical.CompTimezone {
|
||||
tzid, err := child.Props.Text(ical.PropTimezoneID)
|
||||
if err != nil {
|
||||
found := false
|
||||
for _, s := range timezones {
|
||||
if s == tzid {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if found {
|
||||
continue
|
||||
} else {
|
||||
timezones = append(timezones, tzid)
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if child.Name == ical.CompEvent {
|
||||
child.Props.Set(calNameProp)
|
||||
}
|
||||
unified.Children = append(unified.Children, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unifiedCalendar = unified
|
||||
}
|
||||
|
||||
func newHMNCalendar() *ical.Calendar {
|
||||
cal := ical.NewCalendar()
|
||||
|
||||
prodID := ical.NewProp(ical.PropProductID)
|
||||
prodID.SetText("Handmade Network")
|
||||
cal.Props.Set(prodID)
|
||||
|
||||
version := ical.NewProp(ical.PropVersion)
|
||||
version.SetText("1.0")
|
||||
cal.Props.Set(version)
|
||||
|
||||
name := ical.NewProp("X-WR-CALNAME")
|
||||
name.SetText("Handmade Network")
|
||||
cal.Props.Set(name)
|
||||
|
||||
return cal
|
||||
}
|
||||
|
||||
// NOTE(asaf): The ical library we're using doesn't like encoding empty calendars, so we have to do this manually.
|
||||
func emptyCalendarString() []byte {
|
||||
empty := `BEGIN:VCALENDAR
|
||||
VERSION:1.0
|
||||
PRODID:Handmade Network
|
||||
X-WR-CALNAME:Handmade Network
|
||||
END:VCALENDAR
|
||||
`
|
||||
|
||||
return []byte(empty)
|
||||
}
|
|
@ -78,8 +78,6 @@ var Config = HMNConfig{
|
|||
BaseUrl: "https://api.twitch.tv/helix",
|
||||
BaseIDUrl: "https://id.twitch.tv/oauth2",
|
||||
},
|
||||
Calendars: []CalendarSource{
|
||||
},
|
||||
EpisodeGuide: EpisodeGuide{
|
||||
CineraOutputPath: "./annotations/",
|
||||
Projects: map[string]string{"hero": "code", "riscy": "riscy", "bitwise": "bitwise"},
|
||||
|
|
|
@ -28,11 +28,9 @@ type HMNConfig struct {
|
|||
DigitalOcean DigitalOceanConfig
|
||||
Discord DiscordConfig
|
||||
Twitch TwitchConfig
|
||||
Matrix MatrixConfig
|
||||
EpisodeGuide EpisodeGuide
|
||||
DevConfig DevConfig
|
||||
PreviewGeneration PreviewGenerationConfig
|
||||
Calendars []CalendarSource
|
||||
}
|
||||
|
||||
type PostgresConfig struct {
|
||||
|
@ -95,18 +93,6 @@ type TwitchConfig struct {
|
|||
BaseIDUrl string
|
||||
}
|
||||
|
||||
type MatrixConfig struct {
|
||||
Username string
|
||||
Password string
|
||||
BaseUrl string
|
||||
AnnouncementsRoomID string
|
||||
}
|
||||
|
||||
type CalendarSource struct {
|
||||
Name string
|
||||
Url string
|
||||
}
|
||||
|
||||
type EpisodeGuide struct {
|
||||
CineraOutputPath string
|
||||
Projects map[string]string // NOTE(asaf): Maps from slugs to default episode guide topic
|
||||
|
|
|
@ -23,34 +23,6 @@ import (
|
|||
"github.com/jpillora/backoff"
|
||||
)
|
||||
|
||||
type BotEvent struct {
|
||||
Timestamp time.Time
|
||||
Name string
|
||||
Extra string
|
||||
}
|
||||
|
||||
var botEvents = make([]BotEvent, 0, 1000)
|
||||
var botEventsMutex = sync.Mutex{}
|
||||
|
||||
func RecordBotEvent(name, extra string) {
|
||||
botEventsMutex.Lock()
|
||||
defer botEventsMutex.Unlock()
|
||||
if len(botEvents) > 1000 {
|
||||
botEvents = botEvents[len(botEvents)-500:]
|
||||
}
|
||||
botEvents = append(botEvents, BotEvent{
|
||||
Timestamp: time.Now(),
|
||||
Name: name,
|
||||
Extra: extra,
|
||||
})
|
||||
}
|
||||
|
||||
func GetBotEvents() []BotEvent {
|
||||
botEventsMutex.Lock()
|
||||
defer botEventsMutex.Unlock()
|
||||
return botEvents[:]
|
||||
}
|
||||
|
||||
func RunDiscordBot(ctx context.Context, dbConn *pgxpool.Pool) jobs.Job {
|
||||
log := logging.ExtractLogger(ctx).With().Str("module", "discord").Logger()
|
||||
ctx = logging.AttachLoggerToContext(&log, ctx)
|
||||
|
@ -84,11 +56,6 @@ func RunDiscordBot(ctx context.Context, dbConn *pgxpool.Pool) jobs.Job {
|
|||
log.Info().Msg("Connecting to the Discord gateway")
|
||||
bot := newBotInstance(dbConn)
|
||||
err := bot.Run(ctx)
|
||||
disconnectMessage := ""
|
||||
if err != nil {
|
||||
disconnectMessage = err.Error()
|
||||
}
|
||||
RecordBotEvent("Disconnected", disconnectMessage)
|
||||
if err != nil {
|
||||
dur := boff.Duration()
|
||||
log.Error().
|
||||
|
@ -134,8 +101,6 @@ type botInstance struct {
|
|||
conn *websocket.Conn
|
||||
dbConn *pgxpool.Pool
|
||||
|
||||
resuming bool
|
||||
|
||||
heartbeatIntervalMs int
|
||||
forceHeartbeat chan struct{}
|
||||
|
||||
|
@ -228,7 +193,6 @@ func (bot *botInstance) Run(ctx context.Context) (err error) {
|
|||
logging.ExtractLogger(ctx).Info().Msg("Discord asked us to reconnect to the gateway")
|
||||
return nil
|
||||
case OpcodeInvalidSession:
|
||||
RecordBotEvent("Failed to resume - invalid session", "")
|
||||
// We tried to resume but the session was invalid.
|
||||
// Delete the session and reconnect from scratch again.
|
||||
_, err := bot.dbConn.Exec(ctx, `DELETE FROM discord_session`)
|
||||
|
@ -300,11 +264,8 @@ func (bot *botInstance) connect(ctx context.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
RecordBotEvent("Connected", "")
|
||||
if shouldResume {
|
||||
RecordBotEvent("Resuming with session ID", session.ID)
|
||||
// Reconnect to the previous session
|
||||
bot.resuming = true
|
||||
err := bot.sendGatewayMessage(ctx, GatewayMessage{
|
||||
Opcode: OpcodeResume,
|
||||
Data: Resume{
|
||||
|
@ -579,20 +540,11 @@ func (bot *botInstance) processEventMsg(ctx context.Context, msg *GatewayMessage
|
|||
panic(fmt.Sprintf("processEventMsg must only be used on Dispatch messages (opcode %d). Validate this before you call this function.", OpcodeDispatch))
|
||||
}
|
||||
|
||||
if bot.resuming {
|
||||
name := ""
|
||||
if msg.EventName != nil {
|
||||
name = *msg.EventName
|
||||
}
|
||||
RecordBotEvent("Got event while resuming", name)
|
||||
}
|
||||
switch *msg.EventName {
|
||||
case "RESUMED":
|
||||
// Nothing to do, but at least we can log something
|
||||
logging.ExtractLogger(ctx).Info().Msg("Finished resuming gateway session")
|
||||
|
||||
bot.resuming = false
|
||||
RecordBotEvent("Done resuming", "")
|
||||
bot.createApplicationCommands(ctx)
|
||||
case "MESSAGE_CREATE":
|
||||
newMessage := *MessageFromMap(msg.Data, "")
|
||||
|
|
|
@ -185,7 +185,6 @@ func Scrape(ctx context.Context, dbConn *pgxpool.Pool, channelID string, earlies
|
|||
return true
|
||||
}
|
||||
|
||||
msg.Backfilled = true
|
||||
err := HandleIncomingMessage(ctx, dbConn, &msg, createSnippets)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -23,15 +19,9 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/parsing"
|
||||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var autostoreChannels = []string{
|
||||
config.Config.Discord.ShowcaseChannelID,
|
||||
// TODO(asaf): Add jam channel
|
||||
}
|
||||
|
||||
func HandleIncomingMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message, createSnippets bool) error {
|
||||
deleted := false
|
||||
var err error
|
||||
|
@ -46,10 +36,6 @@ func HandleIncomingMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message
|
|||
deleted, err = CleanUpShowcase(ctx, dbConn, msg)
|
||||
}
|
||||
|
||||
if !deleted && err == nil {
|
||||
err = ShareToMatrix(ctx, msg)
|
||||
}
|
||||
|
||||
if !deleted && err == nil {
|
||||
err = MaybeInternMessage(ctx, dbConn, msg)
|
||||
}
|
||||
|
@ -58,11 +44,6 @@ func HandleIncomingMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message
|
|||
err = HandleInternedMessage(ctx, dbConn, msg, deleted, createSnippets)
|
||||
}
|
||||
|
||||
// when we needed her most...she vanished
|
||||
// if !deleted && err == nil {
|
||||
// err = FreyaMode(ctx, dbConn, msg)
|
||||
// }
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -75,7 +56,17 @@ func CleanUpShowcase(ctx context.Context, dbConn db.ConnOrTx, msg *Message) (boo
|
|||
return deleted, nil
|
||||
}
|
||||
|
||||
if !messageShouldBeStored(msg) {
|
||||
hasGoodContent := true
|
||||
if msg.OriginalHasFields("content") && !messageHasLinks(msg.Content) {
|
||||
hasGoodContent = false
|
||||
}
|
||||
|
||||
hasGoodAttachments := true
|
||||
if msg.OriginalHasFields("attachments") && len(msg.Attachments) == 0 {
|
||||
hasGoodAttachments = false
|
||||
}
|
||||
|
||||
if !hasGoodContent && !hasGoodAttachments {
|
||||
err := DeleteMessage(ctx, msg.ChannelID, msg.ID)
|
||||
if err != nil {
|
||||
return deleted, oops.New(err, "failed to delete message")
|
||||
|
@ -91,7 +82,7 @@ func CleanUpShowcase(ctx context.Context, dbConn db.ConnOrTx, msg *Message) (boo
|
|||
err = SendMessages(ctx, dbConn, MessageToSend{
|
||||
ChannelID: channel.ID,
|
||||
Req: CreateMessageRequest{
|
||||
Content: "Posts in #project-showcase are required to have either an image/video or a link, or start with `!til`. Discuss showcase content in #projects.",
|
||||
Content: "Posts in #project-showcase are required to have either an image/video or a link. Discuss showcase content in #projects.",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -146,163 +137,8 @@ func CleanUpLibrary(ctx context.Context, dbConn db.ConnOrTx, msg *Message) (bool
|
|||
return deleted, nil
|
||||
}
|
||||
|
||||
func FreyaMode(ctx context.Context, dbConn db.ConnOrTx, msg *Message) error {
|
||||
if msg.Author.IsBot {
|
||||
return nil
|
||||
}
|
||||
if msg.ChannelID == config.Config.Discord.ShowcaseChannelID || msg.ChannelID == config.Config.Discord.LibraryChannelID {
|
||||
return nil
|
||||
}
|
||||
|
||||
twitteryUrls := []string{
|
||||
"https://twitter.com",
|
||||
"https://x.com",
|
||||
"https://vxtwitter.com",
|
||||
"https://fxtwitter.com",
|
||||
}
|
||||
isTwittery := false
|
||||
for _, url := range twitteryUrls {
|
||||
if strings.Contains(msg.Content, url) {
|
||||
isTwittery = true
|
||||
}
|
||||
}
|
||||
if !isTwittery {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FREYA MODE ENGAGED
|
||||
approvedTweets := []string{
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1757836988495847568",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1752441092501361103",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1753813557966217268",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1746228932188295579",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1732687685850894799",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1761487879178736048",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1733820461492863442",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1732845451701871101",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1765680355657359585",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1784678195997852129",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1741468609044508831",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1759306434053870012",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1754929898492162178",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1782498313511534822",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1623737764041695232",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1718979996125925494",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1675945798448607248",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1662229911375953922",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1652235944752185345",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1386408507218427905",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1436696408506212353",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1444755552777670657",
|
||||
"https://vxtwitter.com/FreyaHolmer/status/1232826293902888960",
|
||||
}
|
||||
tweet := approvedTweets[rand.Intn(len(approvedTweets))]
|
||||
err := SendMessages(ctx, dbConn, MessageToSend{
|
||||
ChannelID: msg.ChannelID,
|
||||
Req: CreateMessageRequest{
|
||||
Content: fmt.Sprintf("No. Only Freya is allowed to tweet. %s", tweet),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to send Freya tweet")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ShareToMatrix(ctx context.Context, msg *Message) error {
|
||||
if msg.Flags&MessageFlagCrossposted == 0 {
|
||||
return nil
|
||||
}
|
||||
if config.Config.Matrix.Username == "" {
|
||||
logging.ExtractLogger(ctx).Warn().Msg("No Matrix user provided; Discord announcement will not be shared")
|
||||
}
|
||||
|
||||
fullMsg, err := GetChannelMessage(ctx, msg.ChannelID, msg.ID)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to get published message contents")
|
||||
}
|
||||
|
||||
bodyMarkdown := CleanUpMarkdown(ctx, fullMsg.Content)
|
||||
bodyHTML := parsing.ParseMarkdown(bodyMarkdown, parsing.DiscordMarkdown)
|
||||
|
||||
// Log in to Matrix (we don't bother to keep access tokens around)
|
||||
var accessToken string
|
||||
{
|
||||
type MatrixLogin struct {
|
||||
Type string `json:"type"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
type MatrixLoginResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
body := MatrixLogin{
|
||||
Type: "m.login.password",
|
||||
User: config.Config.Matrix.Username,
|
||||
Password: config.Config.Matrix.Password,
|
||||
}
|
||||
bodyBytes := utils.Must1(json.Marshal(body))
|
||||
res, err := http.Post(
|
||||
"https://matrix.handmadecities.com/_matrix/client/r0/login",
|
||||
"application/json",
|
||||
bytes.NewReader(bodyBytes),
|
||||
)
|
||||
if err != nil || res.StatusCode >= 300 {
|
||||
return oops.New(err, "failed to log into Matrix")
|
||||
}
|
||||
defer res.Body.Close()
|
||||
resBodyBytes := utils.Must1(io.ReadAll(res.Body))
|
||||
var resBody MatrixLoginResponse
|
||||
utils.Must(json.Unmarshal(resBodyBytes, &resBody))
|
||||
|
||||
accessToken = resBody.AccessToken
|
||||
}
|
||||
|
||||
// Create message
|
||||
{
|
||||
type MessageEvent struct {
|
||||
MsgType string `json:"msgtype"`
|
||||
Body string `json:"body"`
|
||||
Format string `json:"format,omitempty"`
|
||||
FormattedBody string `json:"formatted_body,omitempty"`
|
||||
}
|
||||
tid := "hmn" + strconv.Itoa(rand.Int())
|
||||
body := MessageEvent{
|
||||
MsgType: "m.text",
|
||||
Body: bodyMarkdown,
|
||||
Format: "org.matrix.custom.html",
|
||||
FormattedBody: bodyHTML,
|
||||
}
|
||||
bodyBytes := utils.Must1(json.Marshal(body))
|
||||
req := utils.Must1(http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPut,
|
||||
fmt.Sprintf(
|
||||
"%s/_matrix/client/v3/rooms/%s/send/m.room.message/%s",
|
||||
config.Config.Matrix.BaseUrl,
|
||||
config.Config.Matrix.AnnouncementsRoomID,
|
||||
tid,
|
||||
),
|
||||
bytes.NewReader(bodyBytes),
|
||||
))
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil || res.StatusCode >= 300 {
|
||||
return oops.New(err, "failed to send Matrix message")
|
||||
}
|
||||
}
|
||||
|
||||
logging.ExtractLogger(ctx).Info().
|
||||
Str("contents", bodyMarkdown).
|
||||
Msg("Published Discord announcement to Matrix")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func MaybeInternMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message) error {
|
||||
if messageShouldBeStored(msg) {
|
||||
if msg.ChannelID == config.Config.Discord.ShowcaseChannelID {
|
||||
err := InternMessage(ctx, dbConn, msg)
|
||||
if errors.Is(err, errNotEnoughInfo) {
|
||||
logging.ExtractLogger(ctx).Warn().
|
||||
|
@ -354,8 +190,8 @@ func InternMessage(
|
|||
|
||||
_, err = dbConn.Exec(ctx,
|
||||
`
|
||||
INSERT INTO discord_message (id, channel_id, guild_id, url, user_id, sent_at, snippet_created, backfilled)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
INSERT INTO discord_message (id, channel_id, guild_id, url, user_id, sent_at, snippet_created)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`,
|
||||
msg.ID,
|
||||
msg.ChannelID,
|
||||
|
@ -364,7 +200,6 @@ func InternMessage(
|
|||
msg.Author.ID,
|
||||
msg.Time(),
|
||||
false,
|
||||
msg.Backfilled,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to save new discord message")
|
||||
|
@ -406,7 +241,7 @@ func FetchInternedMessage(ctx context.Context, dbConn db.ConnOrTx, msgId string)
|
|||
// 1. Saves/updates content
|
||||
// 2. Saves/updates snippet
|
||||
// 3. Deletes content/snippet
|
||||
func HandleInternedMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message, removeInternedMessage bool, createSnippet bool) error {
|
||||
func HandleInternedMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message, deleted bool, createSnippet bool) error {
|
||||
tx, err := dbConn.Begin(ctx)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to start transaction")
|
||||
|
@ -417,11 +252,7 @@ func HandleInternedMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message
|
|||
if err != nil && !errors.Is(err, db.NotFound) {
|
||||
return err
|
||||
} else if err == nil {
|
||||
if !removeInternedMessage {
|
||||
removeInternedMessage = !messageShouldBeStored(msg)
|
||||
}
|
||||
|
||||
if !removeInternedMessage {
|
||||
if !deleted {
|
||||
err = SaveMessageContents(ctx, tx, interned, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -854,7 +685,7 @@ func HandleSnippetForInternedMessage(ctx context.Context, dbConn db.ConnOrTx, in
|
|||
|
||||
if interned.MessageContent == nil {
|
||||
// NOTE(asaf): Can't have a snippet without content
|
||||
// NOTE(asaf): Messages that only have an attachment also have a content struct with an empty content string
|
||||
// NOTE(asaf): Messages that only have an attachment also have blank content
|
||||
// TODO(asaf): Do we need to delete existing snippets in this case??? Not entirely sure how to trigger this through discord
|
||||
return nil
|
||||
}
|
||||
|
@ -904,42 +735,43 @@ func HandleSnippetForInternedMessage(ctx context.Context, dbConn db.ConnOrTx, in
|
|||
if shouldCreate {
|
||||
// Get an asset ID or URL to make a snippet from
|
||||
assetId, url, err := getSnippetAssetOrUrl(ctx, tx, &interned.Message)
|
||||
if assetId != nil || url != nil {
|
||||
contentMarkdown := interned.MessageContent.LastContent
|
||||
contentHTML := parsing.ParseMarkdown(contentMarkdown, parsing.DiscordMarkdown)
|
||||
|
||||
contentMarkdown := interned.MessageContent.LastContent
|
||||
contentHTML := parsing.ParseMarkdown(contentMarkdown, parsing.DiscordMarkdown)
|
||||
_, err = tx.Exec(ctx,
|
||||
`
|
||||
INSERT INTO snippet (url, "when", description, _description_html, asset_id, discord_message_id, owner_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`,
|
||||
url,
|
||||
interned.Message.SentAt,
|
||||
contentMarkdown,
|
||||
contentHTML,
|
||||
assetId,
|
||||
interned.Message.ID,
|
||||
interned.HMNUser.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to create snippet from attachment")
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx,
|
||||
`
|
||||
INSERT INTO snippet (url, "when", description, _description_html, asset_id, discord_message_id, owner_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`,
|
||||
url,
|
||||
interned.Message.SentAt,
|
||||
contentMarkdown,
|
||||
contentHTML,
|
||||
assetId,
|
||||
interned.Message.ID,
|
||||
interned.HMNUser.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to create snippet from attachment")
|
||||
}
|
||||
existingSnippet, err = FetchSnippetForMessage(ctx, tx, interned.Message.ID)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to fetch newly-created snippet")
|
||||
}
|
||||
|
||||
existingSnippet, err = FetchSnippetForMessage(ctx, tx, interned.Message.ID)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to fetch newly-created snippet")
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx,
|
||||
`
|
||||
UPDATE discord_message
|
||||
SET snippet_created = TRUE
|
||||
WHERE id = $1
|
||||
`,
|
||||
interned.Message.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to mark message as having snippet")
|
||||
_, err = tx.Exec(ctx,
|
||||
`
|
||||
UPDATE discord_message
|
||||
SET snippet_created = TRUE
|
||||
WHERE id = $1
|
||||
`,
|
||||
interned.Message.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to mark message as having snippet")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1081,36 +913,3 @@ func messageHasLinks(content string) bool {
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
func messageShouldBeStored(msg *Message) bool {
|
||||
if msg == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(msg.Content)), "!til") {
|
||||
return true
|
||||
}
|
||||
|
||||
autostore := false
|
||||
for _, cid := range autostoreChannels {
|
||||
if msg.ChannelID == cid {
|
||||
autostore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if autostore {
|
||||
hasGoodContent := true
|
||||
if msg.OriginalHasFields("content") && !messageHasLinks(msg.Content) {
|
||||
hasGoodContent = false
|
||||
}
|
||||
|
||||
hasGoodAttachments := true
|
||||
if msg.OriginalHasFields("attachments") && len(msg.Attachments) == 0 {
|
||||
hasGoodAttachments = false
|
||||
}
|
||||
|
||||
return hasGoodContent || hasGoodAttachments
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -244,38 +244,20 @@ const (
|
|||
MessageTypeGuildInviteReminder MessageType = 22
|
||||
)
|
||||
|
||||
type MessageFlags int
|
||||
|
||||
const (
|
||||
MessageFlagCrossposted MessageFlags = 1 << iota
|
||||
MessageFlagIsCrosspost
|
||||
MessageFlagSuppressEmbeds
|
||||
MessageFlagSourceMessageDeleted
|
||||
MessageFlagUrgent
|
||||
MessageFlagHasThread
|
||||
MessageFlagEphemeral
|
||||
MessageFlagLoading
|
||||
MessageFlagFailedToMentionSomeRolesInThread
|
||||
MessageFlagSuppressNotifications
|
||||
MessageFlagIsVoiceMessage
|
||||
)
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object
|
||||
type Message struct {
|
||||
ID string `json:"id"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
GuildID *string `json:"guild_id"`
|
||||
Content string `json:"content"`
|
||||
Author *User `json:"author"` // note that this may not be an actual valid user (see the docs)
|
||||
Timestamp string `json:"timestamp"`
|
||||
Type MessageType `json:"type"`
|
||||
Flags MessageFlags `json:"flags"`
|
||||
ID string `json:"id"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
GuildID *string `json:"guild_id"`
|
||||
Content string `json:"content"`
|
||||
Author *User `json:"author"` // note that this may not be an actual valid user (see the docs)
|
||||
Timestamp string `json:"timestamp"`
|
||||
Type MessageType `json:"type"`
|
||||
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
Embeds []Embed `json:"embeds"`
|
||||
|
||||
originalMap map[string]interface{}
|
||||
Backfilled bool
|
||||
}
|
||||
|
||||
func (m *Message) JumpURL() string {
|
||||
|
@ -335,7 +317,6 @@ func MessageFromMap(m interface{}, k string) *Message {
|
|||
Author: UserFromMap(m, "author"),
|
||||
Timestamp: maybeString(mmap, "timestamp"),
|
||||
Type: MessageType(maybeInt(mmap, "type")),
|
||||
Flags: MessageFlags(maybeInt(mmap, "flags")),
|
||||
|
||||
originalMap: mmap,
|
||||
}
|
||||
|
@ -1022,11 +1003,3 @@ func maybeBoolP(m map[string]interface{}, k string) *bool {
|
|||
boolval := val.(bool)
|
||||
return &boolval
|
||||
}
|
||||
|
||||
func maybeArray(m map[string]any, k string) []any {
|
||||
val, ok := m[k]
|
||||
if !ok || val == nil {
|
||||
return nil
|
||||
}
|
||||
return val.([]any)
|
||||
}
|
||||
|
|
|
@ -62,17 +62,6 @@ var WRJ2023 = Jam{
|
|||
UrlSlug: "2023",
|
||||
}
|
||||
|
||||
var LJ2024 = Jam{
|
||||
Event: Event{
|
||||
StartTime: time.Date(2024, 3, 15, 17, 0, 0, 0, time.UTC),
|
||||
EndTime: time.Date(2024, 3, 25, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
Name: "Learning Jam 2024",
|
||||
Slug: "LJ2024",
|
||||
UrlSlug: "learning-2024",
|
||||
}
|
||||
|
||||
// Conferences
|
||||
var HMS2022 = Event{
|
||||
StartTime: time.Date(2022, 11, 16, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
EndTime: time.Date(2022, 11, 18, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
|
@ -88,17 +77,7 @@ var HMBoston2023 = Event{
|
|||
EndTime: time.Date(2023, 8, 4, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
}
|
||||
|
||||
var HMS2024 = Event{
|
||||
StartTime: time.Date(2024, 11, 20, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
EndTime: time.Date(2024, 11, 22, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
}
|
||||
|
||||
var HMBoston2024 = Event{
|
||||
StartTime: time.Date(2024, 8, 9, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
EndTime: time.Date(2024, 8, 10, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
}
|
||||
|
||||
var AllJams = []Jam{WRJ2021, WRJ2022, VJ2023, WRJ2023, LJ2024}
|
||||
var AllJams = []Jam{WRJ2021, WRJ2022, VJ2023, WRJ2023}
|
||||
|
||||
func CurrentJam() *Jam {
|
||||
now := time.Now()
|
||||
|
|
|
@ -44,6 +44,10 @@ func TestShowcase(t *testing.T) {
|
|||
AssertRegexMatch(t, BuildShowcase(), RegexShowcase, nil)
|
||||
}
|
||||
|
||||
func TestStreams(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildStreams(), RegexStreams, nil)
|
||||
}
|
||||
|
||||
func TestWhenIsIt(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWhenIsIt(), RegexWhenIsIt, nil)
|
||||
}
|
||||
|
@ -139,6 +143,7 @@ func TestFeed(t *testing.T) {
|
|||
|
||||
func TestProjectIndex(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildProjectIndex(1, ""), RegexProjectIndex, nil)
|
||||
AssertRegexMatch(t, BuildProjectIndex(2, ""), RegexProjectIndex, map[string]string{"page": "2"})
|
||||
AssertRegexMatch(t, BuildProjectIndex(1, "test"), RegexProjectIndex, map[string]string{"category": "test"})
|
||||
AssertRegexMatch(t, BuildProjectIndex(2, "test"), RegexProjectIndex, map[string]string{"page": "2", "category": "test"})
|
||||
assert.Panics(t, func() { BuildProjectIndex(0, "") })
|
||||
|
@ -422,16 +427,6 @@ func TestJamFeed2023(t *testing.T) {
|
|||
AssertSubdomain(t, BuildJamFeed2023(), "")
|
||||
}
|
||||
|
||||
func TestJamIndex2024_Learning(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildJamIndex2024_Learning(), RegexJamIndex2024_Learning, nil)
|
||||
AssertSubdomain(t, BuildJamIndex2024_Learning(), "")
|
||||
}
|
||||
|
||||
func TestJamFeed2024_Learning(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildJamFeed2024_Learning(), RegexJamFeed2024_Learning, nil)
|
||||
AssertSubdomain(t, BuildJamFeed2024_Learning(), "")
|
||||
}
|
||||
|
||||
func TestTimeMachine(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildTimeMachine(), RegexTimeMachine, nil)
|
||||
AssertSubdomain(t, BuildTimeMachine(), "")
|
||||
|
@ -459,8 +454,8 @@ func TestTimeMachineFormDone(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewsletterSignup(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildAPINewsletterSignup(), RegexNewsletterSignup, nil)
|
||||
AssertSubdomain(t, BuildAPINewsletterSignup(), "")
|
||||
AssertRegexMatch(t, BuildNewsletterSignup(), RegexNewsletterSignup, nil)
|
||||
AssertSubdomain(t, BuildNewsletterSignup(), "")
|
||||
}
|
||||
|
||||
func TestProjectNewJam(t *testing.T) {
|
||||
|
|
|
@ -35,6 +35,13 @@ func BuildShowcase() string {
|
|||
return Url("/showcase", nil)
|
||||
}
|
||||
|
||||
var RegexStreams = regexp.MustCompile("^/streams$")
|
||||
|
||||
func BuildStreams() string {
|
||||
defer CatchPanic()
|
||||
return Url("/streams", nil)
|
||||
}
|
||||
|
||||
var RegexWhenIsIt = regexp.MustCompile("^/whenisit$")
|
||||
|
||||
func BuildWhenIsIt() string {
|
||||
|
@ -42,6 +49,13 @@ func BuildWhenIsIt() string {
|
|||
return Url("/whenisit", nil)
|
||||
}
|
||||
|
||||
var RegexNewsletterSignup = regexp.MustCompile("^/newsletter$")
|
||||
|
||||
func BuildNewsletterSignup() string {
|
||||
defer CatchPanic()
|
||||
return Url("/newsletter", nil)
|
||||
}
|
||||
|
||||
var RegexJamsIndex = regexp.MustCompile("^/jams$")
|
||||
|
||||
func BuildJamsIndex() string {
|
||||
|
@ -112,34 +126,6 @@ func BuildJamRecap2023_Visibility() string {
|
|||
return Url("/jam/visibility-2023/recap", nil)
|
||||
}
|
||||
|
||||
var RegexJamIndex2024_Learning = regexp.MustCompile("^/jam/learning-2024$")
|
||||
|
||||
func BuildJamIndex2024_Learning() string {
|
||||
defer CatchPanic()
|
||||
return Url("/jam/learning-2024", nil)
|
||||
}
|
||||
|
||||
var RegexJamFeed2024_Learning = regexp.MustCompile("^/jam/learning-2024/feed$")
|
||||
|
||||
func BuildJamFeed2024_Learning() string {
|
||||
defer CatchPanic()
|
||||
return Url("/jam/learning-2024/feed", nil)
|
||||
}
|
||||
|
||||
var RegexJamGuidelines2024_Learning = regexp.MustCompile("^/jam/learning-2024/guidelines$")
|
||||
|
||||
func BuildJamGuidelines2024_Learning() string {
|
||||
defer CatchPanic()
|
||||
return Url("/jam/learning-2024/guidelines", nil)
|
||||
}
|
||||
|
||||
var RegexJamSaveTheDate = regexp.MustCompile("^/jam/upcoming$")
|
||||
|
||||
func BuildJamSaveTheDate() string {
|
||||
defer CatchPanic()
|
||||
return Url("/jam/upcoming", nil)
|
||||
}
|
||||
|
||||
func BuildJamIndexAny(slug string) string {
|
||||
defer CatchPanic()
|
||||
return Url(fmt.Sprintf("/jam/%s", slug), nil)
|
||||
|
@ -185,20 +171,6 @@ func BuildTimeMachineFormDone() string {
|
|||
return Url("/timemachine/thanks", nil)
|
||||
}
|
||||
|
||||
var RegexCalendarIndex = regexp.MustCompile("^/calendar$")
|
||||
|
||||
func BuildCalendarIndex() string {
|
||||
defer CatchPanic()
|
||||
return Url("/calendar", nil)
|
||||
}
|
||||
|
||||
var RegexCalendarICal = regexp.MustCompile("^/Handmade Network.ical$")
|
||||
|
||||
func BuildCalendarICal() string {
|
||||
defer CatchPanic()
|
||||
return Url("/Handmade Network.ical", nil)
|
||||
}
|
||||
|
||||
// QUESTION(ben): Can we change these routes?
|
||||
|
||||
var RegexLoginAction = regexp.MustCompile("^/login$")
|
||||
|
@ -922,12 +894,6 @@ func BuildDiscordShowcaseBacklog() string {
|
|||
return Url("/discord_showcase_backlog", nil)
|
||||
}
|
||||
|
||||
var RegexDiscordBotDebugPage = regexp.MustCompile("^/discord_bot_debug$")
|
||||
|
||||
func BuildDiscordBotDebugPage() string {
|
||||
return Url("/discord_bot_debug", nil)
|
||||
}
|
||||
|
||||
/*
|
||||
* API
|
||||
*/
|
||||
|
@ -938,12 +904,6 @@ func BuildAPICheckUsername() string {
|
|||
return Url("/api/check_username", nil)
|
||||
}
|
||||
|
||||
var RegexAPINewsletterSignup = regexp.MustCompile("^/api/newsletter_signup$")
|
||||
|
||||
func BuildAPINewsletterSignup() string {
|
||||
return Url("/api/newsletter_signup", nil)
|
||||
}
|
||||
|
||||
/*
|
||||
* Twitch stuff
|
||||
*/
|
||||
|
@ -1040,12 +1000,6 @@ func BuildUserFile(filepath string) string {
|
|||
return BuildPublic(fmt.Sprintf("media/%s", filepath), false)
|
||||
}
|
||||
|
||||
/*
|
||||
* Redirects
|
||||
*/
|
||||
|
||||
var RegexUnwind = regexp.MustCompile(`^/unwind$`)
|
||||
|
||||
/*
|
||||
* Other
|
||||
*/
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/migration/types"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerMigration(AddBackfillToDiscordMessage{})
|
||||
}
|
||||
|
||||
type AddBackfillToDiscordMessage struct{}
|
||||
|
||||
func (m AddBackfillToDiscordMessage) Version() types.MigrationVersion {
|
||||
return types.MigrationVersion(time.Date(2024, 3, 28, 18, 41, 7, 0, time.UTC))
|
||||
}
|
||||
|
||||
func (m AddBackfillToDiscordMessage) Name() string {
|
||||
return "AddBackfillToDiscordMessage"
|
||||
}
|
||||
|
||||
func (m AddBackfillToDiscordMessage) Description() string {
|
||||
return "Add a backfill flag to discord messages"
|
||||
}
|
||||
|
||||
func (m AddBackfillToDiscordMessage) Up(ctx context.Context, tx pgx.Tx) error {
|
||||
_, err := tx.Exec(ctx,
|
||||
`
|
||||
ALTER TABLE discord_message
|
||||
ADD COLUMN backfilled BOOLEAN NOT NULL default FALSE;
|
||||
`,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m AddBackfillToDiscordMessage) Down(ctx context.Context, tx pgx.Tx) error {
|
||||
_, err := tx.Exec(ctx,
|
||||
`
|
||||
ALTER TABLE discord_message
|
||||
DROP COLUMN backfilled;
|
||||
`,
|
||||
)
|
||||
return err
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/migration/types"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerMigration(newsletter{})
|
||||
}
|
||||
|
||||
type newsletter struct{}
|
||||
|
||||
func (m newsletter) Version() types.MigrationVersion {
|
||||
return types.MigrationVersion(time.Date(2024, 5, 7, 1, 34, 32, 0, time.UTC))
|
||||
}
|
||||
|
||||
func (m newsletter) Name() string {
|
||||
return "newsletter"
|
||||
}
|
||||
|
||||
func (m newsletter) Description() string {
|
||||
return "Adds the newsletter signup"
|
||||
}
|
||||
|
||||
func (m newsletter) Up(ctx context.Context, tx pgx.Tx) error {
|
||||
_, err := tx.Exec(ctx,
|
||||
`
|
||||
CREATE TABLE newsletter_emails (
|
||||
email VARCHAR(255) NOT NULL PRIMARY KEY
|
||||
);
|
||||
`,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m newsletter) Down(ctx context.Context, tx pgx.Tx) error {
|
||||
_, err := tx.Exec(ctx,
|
||||
`
|
||||
DROP TABLE newsletter_emails;
|
||||
`,
|
||||
)
|
||||
return err
|
||||
}
|
|
@ -167,6 +167,14 @@ article code {
|
|||
margin-right: auto;
|
||||
}
|
||||
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flex-grow-1 {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.flex-fair {
|
||||
flex-basis: 1px;
|
||||
flex-grow: 1;
|
||||
|
@ -402,13 +410,6 @@ article code {
|
|||
.bg--dim-ns {
|
||||
@include usevar(background-color, dim-background);
|
||||
}
|
||||
|
||||
.g0-ns { gap: $spacing-none; }
|
||||
.g1-ns { gap: $spacing-extra-small; }
|
||||
.g2-ns { gap: $spacing-small; }
|
||||
.g3-ns { gap: $spacing-medium; }
|
||||
.g4-ns { gap: $spacing-large; }
|
||||
.g5-ns { gap: $spacing-extra-large; }
|
||||
}
|
||||
|
||||
@media #{$breakpoint-medium} {
|
||||
|
@ -428,13 +429,6 @@ article code {
|
|||
.bg--dim-m {
|
||||
@include usevar(background-color, dim-background);
|
||||
}
|
||||
|
||||
.g0-m { gap: $spacing-none; }
|
||||
.g1-m { gap: $spacing-extra-small; }
|
||||
.g2-m { gap: $spacing-small; }
|
||||
.g3-m { gap: $spacing-medium; }
|
||||
.g4-m { gap: $spacing-large; }
|
||||
.g5-m { gap: $spacing-extra-large; }
|
||||
}
|
||||
|
||||
@media #{$breakpoint-large} {
|
||||
|
@ -454,13 +448,6 @@ article code {
|
|||
.bg--dim-l {
|
||||
@include usevar(background-color, dim-background);
|
||||
}
|
||||
|
||||
.g0-l { gap: $spacing-none; }
|
||||
.g1-l { gap: $spacing-extra-small; }
|
||||
.g2-l { gap: $spacing-small; }
|
||||
.g3-l { gap: $spacing-medium; }
|
||||
.g4-l { gap: $spacing-large; }
|
||||
.g5-l { gap: $spacing-extra-large; }
|
||||
}
|
||||
|
||||
.not-first:first-child {
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/calendar"
|
||||
"git.handmade.network/hmn/hmn/src/hmndata"
|
||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
|
@ -103,9 +102,6 @@ func ProjectToTemplate(
|
|||
func ProjectAndStuffToTemplate(p *hmndata.ProjectAndStuff, url string, theme string) Project {
|
||||
res := ProjectToTemplate(&p.Project, url)
|
||||
res.Logo = ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset, theme)
|
||||
for _, o := range p.Owners {
|
||||
res.Owners = append(res.Owners, UserToTemplate(o, theme))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
|
@ -531,16 +527,6 @@ func EducationArticleToTemplate(a *models.EduArticle) EduArticle {
|
|||
return res
|
||||
}
|
||||
|
||||
func CalendarEventToTemplate(ev *calendar.CalendarEvent) CalendarEvent {
|
||||
return CalendarEvent{
|
||||
Name: ev.Name,
|
||||
Desc: ev.Desc,
|
||||
StartTime: ev.StartTime.UTC(),
|
||||
EndTime: ev.EndTime.UTC(),
|
||||
CalName: ev.CalName,
|
||||
}
|
||||
}
|
||||
|
||||
func maybeString(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
|
|
|
@ -39,16 +39,12 @@
|
|||
{{ end }}
|
||||
|
||||
<!-- Main post -->
|
||||
<div class="{{ if .IsProjectPage }}mb3{{ end }}">
|
||||
<div class="mb3">
|
||||
<div class="post-content overflow-x-auto">
|
||||
{{ .MainPost.Content }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ if not .IsProjectPage }}
|
||||
{{ template "newsletter_signup.html" . }}
|
||||
{{ end }}
|
||||
|
||||
<div class="optionbar"></div>
|
||||
|
||||
{{ range .Comments }}
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
{{ template "base.html" . }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="ph3 ph0-ns">
|
||||
<h2>Future events</h2>
|
||||
<div class="cal_toggles mb2 flex flex-row g2">
|
||||
{{ range .Calendars }}
|
||||
<label class="db br3 pv1 ph2 pointer b--gray ba" for="{{ . }}">
|
||||
<input id="{{ . }}" autocomplete="off" type="checkbox" value="{{ . }}" checked />
|
||||
<span>{{ . }}</span>
|
||||
</label>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="">
|
||||
<a class="ical_link" href="{{ .BaseICalUrl }}">Copy iCal Url</a>
|
||||
</div>
|
||||
<div class="flex flex-column g2">
|
||||
{{ range .Events }}
|
||||
<div data-calname="{{ .CalName }}" class="cal_event timeline-item pa3 br3">
|
||||
<div>{{ timehtmlcontent .StartTime }}</div>
|
||||
<div>
|
||||
<strong class="f4 c--theme">{{ .Name }}</strong>
|
||||
</div>
|
||||
{{ with .Desc }}
|
||||
<div class="mb2">{{ . }}</div>
|
||||
{{ end }}
|
||||
{{ with .CalName }}
|
||||
<div class="dib br2 ba b--gray ph1 f7">{{ . }}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let events = document.querySelectorAll(".cal_event");
|
||||
let toggles = document.querySelectorAll(".cal_toggles input");
|
||||
let icalLink = document.querySelector(".ical_link");
|
||||
let baseICalUrl = icalLink.href;
|
||||
|
||||
function refreshEvents() {
|
||||
let cals = {};
|
||||
for (let i = 0; i < toggles.length; ++i) {
|
||||
cals[toggles[i].id] = toggles[i].checked;
|
||||
}
|
||||
|
||||
for (let i = 0; i < events.length; ++i) {
|
||||
let ev = events[i];
|
||||
let calName = ev.getAttribute("data-calname");
|
||||
if (cals[calName]) {
|
||||
ev.style.display = "block";
|
||||
} else {
|
||||
ev.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
let icalFilter = [];
|
||||
let hasAll = true;
|
||||
for (let i = 0; i < toggles.length; ++i) {
|
||||
if (toggles[i].checked) {
|
||||
icalFilter.push(toggles[i].id);
|
||||
} else {
|
||||
hasAll = false;
|
||||
}
|
||||
}
|
||||
|
||||
icalLink.disabled = false;
|
||||
if (hasAll) {
|
||||
icalLink.href = baseICalUrl;
|
||||
} else if (icalFilter.length == 0) {
|
||||
icalLink.removeAttribute("href");
|
||||
} else {
|
||||
icalFilter.sort();
|
||||
let url = new URL(baseICalUrl);
|
||||
let params = new URLSearchParams();
|
||||
for (let i = 0; i < icalFilter.length; ++i) {
|
||||
params.append(icalFilter[i], "true");
|
||||
}
|
||||
url.search = params.toString();
|
||||
icalLink.href = url.toString();
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < toggles.length; ++i) {
|
||||
toggles[i].addEventListener("input", refreshEvents);
|
||||
}
|
||||
|
||||
icalLink.addEventListener("click", function(ev) {
|
||||
ev.preventDefault();
|
||||
navigator.clipboard.writeText(icalLink.href);
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
|
@ -1,22 +0,0 @@
|
|||
{{ template "base.html" . }}
|
||||
|
||||
{{ define "content" }}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Timestamp</td>
|
||||
<td>Name</td>
|
||||
<td>Extras</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .BotEvents }}
|
||||
<tr>
|
||||
<td>{{ rfc3339 .Timestamp }}</td>
|
||||
<td>{{ .Name }}</td>
|
||||
<td>{{ .Extra }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
|
@ -91,7 +91,6 @@
|
|||
<a href="{{ .Header.FishbowlUrl }}">Fishbowls</a>
|
||||
<a href="{{ .Header.PodcastUrl }}">Podcast</a>
|
||||
<a href="https://guide.handmade-seattle.com/s" target="_blank">Handmade Dev Show</a>
|
||||
{{/*<a href="{{ .Header.CalendarUrl }}">Calendar</a>*/}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="root-item">
|
||||
|
@ -184,18 +183,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
const dtf = new Intl.DateTimeFormat([], {
|
||||
dateStyle: "full",
|
||||
timeStyle: "short",
|
||||
});
|
||||
|
||||
for (const time of document.querySelectorAll('time')) {
|
||||
const d = new Date(Date.parse(time.dateTime));
|
||||
if (time.getAttribute("data-type") == "content") {
|
||||
time.textContent = dtf.format(d);
|
||||
} else {
|
||||
time.title = dtf.format(d);
|
||||
}
|
||||
time.title = d.toLocaleString();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
<div class="mw7 margin-center link--white">
|
||||
<div class="flex flex-column pv4 tc">
|
||||
<img class="jam-logo margin-center" src="{{ static "learningjam2024/logo.svg" }}">
|
||||
<div class="margin-center mt3 mt4-ns">
|
||||
<a class="jam-title db mb3" href="{{ .JamUrl }}">Learning Jam</a>
|
||||
<div class="flex flex-column flex-row-ns justify-between lh-solid g3 g0-ns">
|
||||
<div>
|
||||
<div class="fw7 f3 mb1">Learn</div>
|
||||
<div class="fw3 f4">March 15-17, 2024</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw7 f3 mb1">Share</div>
|
||||
<div class="fw3 f4">March 22-24, 2024</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ if gt .DaysUntilEnd 0 }}
|
||||
<div class="flex g3 justify-center mt4">
|
||||
<a href="{{ .DiscordInviteUrl }}" class="btn--jam">Join the Discord</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
|
@ -1,19 +0,0 @@
|
|||
<div class="mw7 margin-center link--white">
|
||||
<div class="flex flex-column flex-row-ns justify-center items-center pv4 g3 tc tl-ns">
|
||||
<img class="jam-logo-small" src="{{ static "learningjam2024/logo.svg" }}">
|
||||
<div class="flex flex-column justify-between">
|
||||
<div>
|
||||
<a class="jam-title db small" href="{{ .JamUrl }}">Learning Jam</a>
|
||||
<div class="fw6 f5 mb3">March 15 - 24, 2024</div>
|
||||
</div>
|
||||
<div class="flex items-center g2">
|
||||
{{ if and (not .SubmittedProjectUrl) (gt .DaysUntilEnd 0) }}
|
||||
<a href="{{ .NewProjectUrl }}" class="btn--jam">Create your project</a>
|
||||
{{ else }}
|
||||
<a href="{{ .DiscordInviteUrl }}" class="btn--jam">Join the Discord</a>
|
||||
{{ end }}
|
||||
<a href="{{ .GuidelinesUrl }}" class="flex items-baseline pa2">Guidelines <div class="dib svgicon f8 ml1">{{ svg "chevron-right" }}</div></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,14 +0,0 @@
|
|||
<div class="ph3 pv4 bb b--rich-gray">
|
||||
<h2 class="dib c--theme-gradient-light">What is a Learning Jam?</h2>
|
||||
<div class="post-content">
|
||||
<p>
|
||||
The <b class="c--theme-gradient-light">Learning Jam</b> is an opportunity for you to learn something new.
|
||||
</p>
|
||||
<p>
|
||||
Unlike traditional game jams, and unlike our previous programming jams, the goal of the <b class="c--theme-gradient-light">Learning Jam</b> is <em>knowledge</em>. It's an opportunity to throw yourself at a topic and learn everything you can about it—and then, to turn around and share that knowledge with others.
|
||||
</p>
|
||||
<p>
|
||||
The jam takes place over <b>two weekends</b>. On the first weekend, you'll learn as much as you can about your topic of choice. On the second, you'll share what you learned in whatever form you like—writeup, video, Minecraft mod, whatever.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
|
@ -1,41 +0,0 @@
|
|||
{{ range . }}
|
||||
<div class="flex flex-column g3 bg--rich-gray pa3">
|
||||
<div class="flex flex-row g3 items-start">
|
||||
{{ if .OwnerAvatarUrl }}
|
||||
<a class="flex flex-shrink-0 br-100 square items-center justify-center overflow-hidden" href="{{ .OwnerUrl }}">
|
||||
<img class="user-avatar w2-5" src="{{ .OwnerAvatarUrl }}" />
|
||||
</a>
|
||||
{{ end }}
|
||||
<div class="flex flex-column">
|
||||
<div class="">
|
||||
<a title="{{ (index .Projects 0).Blurb }}" href="{{ (index .Projects 0).Url }}" class="link--white fw6">{{ (index .Projects 0).Name }}</a>
|
||||
by
|
||||
<a href="{{ .OwnerUrl }}" class="link--white fw6">{{ .OwnerName }}</a>
|
||||
<span class="f7 c--gray nowrap">{{ timehtml (relativedate .Date) .Date }}</span>
|
||||
</div>
|
||||
<div>{{ trim .Description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ range .EmbedMedia }}
|
||||
<div class="flex flex-column {{ if eq .Type mediaembed }}wide-screen{{ end }} justify-stretch iframe-fill">
|
||||
{{ if eq .Type mediaimage }}
|
||||
<img src="{{ .AssetUrl }}">
|
||||
{{ else if eq .Type mediavideo }}
|
||||
{{ if .ThumbnailUrl }}
|
||||
<video src="{{ .AssetUrl }}" poster="{{ .ThumbnailUrl }}" preload="none" controls>
|
||||
{{ else }}
|
||||
<video src="{{ .AssetUrl }}" preload="metadata" controls>
|
||||
{{ end }}
|
||||
{{ else if eq .Type mediaaudio }}
|
||||
<audio src="{{ .AssetUrl }}" controls>
|
||||
{{ else if eq .Type mediaembed }}
|
||||
{{ .EmbedHTML }}
|
||||
{{ else }}
|
||||
<div class="">
|
||||
<a href="{{ .AssetUrl }}" target="_blank">💾{{ .Filename }} ({{ filesize .FileSize }})</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
|
@ -1,100 +0,0 @@
|
|||
<style>
|
||||
.lightbulb-icon {
|
||||
--mask-url: url("{{ static "learningjam2024/lightbulb.svg" }}");
|
||||
}
|
||||
.books-icon {
|
||||
--mask-url: url("{{ static "learningjam2024/books.svg" }}");
|
||||
}
|
||||
.presentation-icon {
|
||||
--mask-url: url("{{ static "learningjam2024/presentation.svg" }}");
|
||||
}
|
||||
</style>
|
||||
<div class="ph3 pv4 bb b--rich-gray">
|
||||
<h2 class="dib c--theme-gradient-light">What is a Learning Jam?</h2>
|
||||
<div class="post-content">
|
||||
<p>
|
||||
The <b class="c--theme-gradient-light">Learning Jam</b> is an opportunity for you to learn something new.
|
||||
</p>
|
||||
<p>
|
||||
Unlike traditional game jams, and unlike our previous programming jams, the goal of the <b class="c--theme-gradient-light">Learning Jam</b> is <em>knowledge</em>. It's an opportunity to throw yourself at a topic and learn everything you can about it—and then, to turn around and share that knowledge with others.
|
||||
</p>
|
||||
<p>
|
||||
The jam takes place over <b>two weekends</b>. On the first weekend, you'll learn as much as you can about your topic of choice. On the second, you'll share what you learned in whatever form you like—writeup, video, Minecraft mod, whatever.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ph3 pv4 bb b--rich-gray">
|
||||
<h2 class="mb3 dib c--theme-gradient-light">How to participate</h2>
|
||||
<div class="flex flex-column g2">
|
||||
<div class="pa3 flex flex-column flex-row-ns g3 items-center bg--rich-gray">
|
||||
<div class="flex-shrink-0 w4 flex justify-center items-center">
|
||||
<div class="flex svg-mask lightbulb-icon bg--theme-gradient-light" style="width: 5.2rem">
|
||||
<img class="invisible" src="{{ static "learningjam2024/lightbulb.svg" }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<h3 class="f4">Choose a topic.</h3>
|
||||
<p>
|
||||
Decide what you'll spend your weekend learning about. Maybe there's an area of programming you've been meaning to dig into, or maybe there's a specialization in your field that you've been curious about. Maybe you just need a reason to read papers for a weekend.
|
||||
</p>
|
||||
<p>
|
||||
You're welcome to work in teams or work solo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pa3 flex flex-column flex-row-ns g3 items-center bg--rich-gray">
|
||||
<div class="flex-shrink-0 w4 flex justify-center items-center">
|
||||
<div class="flex flex-shrink-0 svg-mask books-icon bg--theme-gradient-light" style="width: 3.6rem">
|
||||
<img class="invisible" src="{{ static "learningjam2024/books.svg" }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<h3 class="f4">Mar 15-17: Learn!</h3>
|
||||
<p>
|
||||
{{ if and (eq .DaysUntilStart 0) (not .SubmittedProjectUrl) }}
|
||||
<a href="{{ .NewProjectUrl }}">Create a Handmade Network project</a>
|
||||
{{ else }}
|
||||
Create a Handmade Network project
|
||||
{{ end }}
|
||||
to track your progress. Then go down the rabbit hole. Absorb as much information as you can in a weekend.
|
||||
</p>
|
||||
<p>
|
||||
As you learn, we encourage you to share updates on Discord using the <b>!til</b> command. These updates will be published as part of your submission.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pa3 flex flex-column flex-row-ns g3 items-center bg--rich-gray">
|
||||
<div class="flex-shrink-0 w4 flex justify-center items-center">
|
||||
<div class="flex flex-shrink-0 svg-mask presentation-icon bg--theme-gradient-light" style="width: 5rem">
|
||||
<img class="invisible" src="{{ static "learningjam2024/presentation.svg" }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<h3 class="f4">Mar 22-24: Share!</h3>
|
||||
<p>
|
||||
Share what you learned with the rest of the community. You can present the information in any form you like—whatever helps you communicate most effectively.
|
||||
</p>
|
||||
<p>
|
||||
Your post doesn't have to be the authoritative resource on a subject. It doesn't even have to do much "teaching" at all. The point is just to share what you learned, what surprised you, what you found interesting, and where you want to go from here.
|
||||
</p>
|
||||
<p>
|
||||
Your Handmade Network project is your final submission. Make sure the description gives adequate context, and either attach or link to your final presentation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ph3 pv4 bb b--rich-gray">
|
||||
<h2 class="dib c--theme-gradient-light">Why?</h2>
|
||||
<div class="post-content">
|
||||
<p>
|
||||
The Handmade Network's goal is to change the software industry by building back up from new foundations. But in order to do that, we need to understand those foundations.
|
||||
</p>
|
||||
<p>
|
||||
Our other programming jams are great opportunities to try building new things. But what should those new things be? We want to give the community a chance to focus just on learning and research, to gather information and share it with others, to boost the entire community's knowledge on a topic.
|
||||
</p>
|
||||
<p>
|
||||
We hope that the results of the <b class="c--theme-gradient-light">Learning Jam</b> inspire new ideas for the community to explore in the Visibility and Wheel Reinvention jams later in the year. There is so much bad software out in the world, and this is the first step toward reinventing it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
|
@ -1,35 +0,0 @@
|
|||
{{ if .Projects }}
|
||||
<div class="flex flex-row flex-wrap g2 link--white">
|
||||
{{ range .Projects }}
|
||||
<div class="flex-basis-40-ns flex-grow-1 flex-shrink-1 bg--rich-gray pa3 flex flex-column g1">
|
||||
{{ if gt (len .Owners) 1 }}
|
||||
<div class="flex g2">
|
||||
{{ range .Owners }}
|
||||
<a title="{{ .Name }}" href="{{ .ProfileUrl }}" class="flex">
|
||||
<img src="{{ .AvatarUrl }}" class="user-avatar w2">
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else }}
|
||||
{{ range .Owners }}
|
||||
<a href="{{ .ProfileUrl }}" class="fw6 link--white flex g2 items-center">
|
||||
<img src="{{ .AvatarUrl }}" class="user-avatar w2">
|
||||
<div>
|
||||
{{ .Name }}
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<div class="f3 mt2">
|
||||
<a href="{{ .Url }}" class="fw6 db">{{ .Name }}</a>
|
||||
</div>
|
||||
<div class="flex-grow-1">{{ .Blurb }}</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if isodd (len .Projects) }}
|
||||
<div class="flex-basis-40-ns flex-grow-1 flex-shrink-1"></div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else }}
|
||||
No projects have been created yet. <a href="{{ .NewProjectUrl }}">Be the first!</a>
|
||||
{{ end }}
|
|
@ -1,46 +0,0 @@
|
|||
<div class="pv3 flex flex-column g2 items-center">
|
||||
<form id="newsletter-form" class="flex flex-column items-center">
|
||||
<div class="b pv2">Sign up for our email newsletter:</div>
|
||||
<div class="flex g2 justify-center">
|
||||
<input id="newsletter-email" type="email" class="input--jam flex-grow-1 flex-grow-0-ns" placeholder="me@example.com">
|
||||
<button id="newsletter-button" class="white">Sign up</button>
|
||||
</div>
|
||||
<div id="newsletter-message" class="f7 pt1">
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
{
|
||||
const form = document.querySelector("#newsletter-form");
|
||||
const emailField = document.querySelector("#newsletter-email");
|
||||
const button = document.querySelector("#newsletter-button");
|
||||
const message = document.querySelector("#newsletter-message");
|
||||
|
||||
form.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch("{{ .NewsletterSignupUrl }}", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"email": emailField.value,
|
||||
}),
|
||||
});
|
||||
if (res.status >= 400) {
|
||||
throw new Error("bad request");
|
||||
}
|
||||
|
||||
message.innerText = "Signed up successfully!";
|
||||
} catch (err) {
|
||||
message.innerText = "There was an error signing up.";
|
||||
}
|
||||
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
</script>
|
|
@ -16,18 +16,8 @@
|
|||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-black-20-alternate:nth-of-type(2n) {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
{{ if .TwitchEmbedUrl }}
|
||||
<div class="section mw8 margin-center ph3 ph4-l mv4" style="aspect-ratio: 16 / 9;">
|
||||
<iframe src="{{ .TwitchEmbedUrl }}" allowfullscreen width="100%" height="100%"></iframe>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div id="top-container" class="flex flex-column items-center ph3">
|
||||
<img id="logo" src="{{ static "wheeljam2023/logo.svg" }}">
|
||||
<h1 id="title">Wheel Reinvention Jam</h1>
|
||||
|
@ -76,16 +66,7 @@
|
|||
</div>
|
||||
|
||||
{{ if eq .DaysUntilEnd 0 }}
|
||||
<div class="section bg-black-20-alternate pv4">
|
||||
<div class="mw8 margin-center ph3 ph4-l">
|
||||
<h2>Recap show</h2>
|
||||
<p>Watch the livestream celebrating this year's submissions:</p>
|
||||
<div class="aspect-ratio aspect-ratio--16x9">
|
||||
<iframe class="aspect-ratio--object" src="https://www.youtube-nocookie.com/embed/BuYoXfPZ6cI?si=Wian-rtg8PgPVcU1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section bg-black-20-alternate pv4 overflow-hidden">
|
||||
<div class="section bg-black-20 pv4 overflow-hidden">
|
||||
<div class="mw8 margin-center ph3 ph4-l">
|
||||
<h2>Submitted projects</h2>
|
||||
<div class="mt3 projects g3 back-to-normal">
|
||||
|
@ -99,7 +80,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{{ else if and (eq .DaysUntilStart 0) (not (eq .ShowcaseJson "[]")) }}
|
||||
<div id="showcase-outer-container" class="bg-black-20-alternate pt4 pb3 pb4-ns">
|
||||
<div id="showcase-outer-container" class="bg-black-20 pt4 pb3 pb4-ns">
|
||||
<div class="section mw8 margin-center ph3 ph4-l">
|
||||
{{ if gt .DaysUntilEnd 0 }}
|
||||
<h2>Recent updates</h2>
|
||||
|
@ -223,7 +204,7 @@
|
|||
});
|
||||
</script>
|
||||
{{ else }}
|
||||
<div class="section bg-black-20-alternate pv4 overflow-hidden">
|
||||
<div class="section bg-black-20 pv4 overflow-hidden">
|
||||
<div class="mw8 margin-center ph3 ph4-l">
|
||||
<h2>Last year's entries</h2>
|
||||
<p>
|
||||
|
@ -305,7 +286,7 @@
|
|||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="bg-black-20-alternate pt4 pb3 pb4-ns">
|
||||
<div class="pt4 pb3 pb4-ns">
|
||||
<div class="flex-ns flex-row-ns mw8 margin-center ph3 ph4-l">
|
||||
<div class="section flex-fair mb4 mb0-ns">
|
||||
<h2>Why reinvent the wheel?</h2>
|
||||
|
@ -339,7 +320,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-black-20-alternate pt4 pb3 pb4-ns">
|
||||
<div class="bg-black-20 pt4 pb3 pb4-ns">
|
||||
<div class="section mw8 margin-center ph3 ph4-l">
|
||||
<h2>How to participate</h2>
|
||||
<p>
|
||||
|
@ -383,7 +364,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-black-20-alternate pv4">
|
||||
<div class="pt4">
|
||||
<div class="section mw8 margin-center ph3 ph4-l">
|
||||
<h2>Rules</h2>
|
||||
<ul>
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
{{ template "jam_2024_learning_base.html" . }}
|
||||
|
||||
{{ define "content-top" }}
|
||||
{{ template "jam_2024_lj_bannersmall.html" . }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="mw7 margin-center flex flex-column">
|
||||
<div class="ph3 pv4 bb b--rich-gray">
|
||||
<h2 class="c--theme-gradient-light mb3">Projects</h2>
|
||||
{{ template "jam_2024_lj_projects.html" .Projects }}
|
||||
</div>
|
||||
{{ if .TimelineItems }}
|
||||
<div class="ph3 pv4 bb b--rich-gray">
|
||||
<h2 class="c--theme-gradient-light">All updates</h2>
|
||||
<p>
|
||||
See what community members are learning about their topics. You can share your own updates via your <a href="{{ or .SubmittedProjectUrl .NewProjectUrl }}">project page</a> or using the <b>!til</b> command on Discord.
|
||||
</p>
|
||||
<div class="flex flex-column g2 mt3">
|
||||
{{ template "jam_2024_lj_feeditems.html" .TimelineItems }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div>
|
||||
<div class="mv5 h3 fill-current link--white">
|
||||
<a href="{{ .Header.HMNHomepageUrl }}">{{ svg "hmn_circuit" }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{{ template "jam_2024_learning_base.html" . }}
|
||||
|
||||
{{ define "content-top" }}
|
||||
{{ template "jam_2024_lj_bannersmall.html" . }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="mw7 margin-center flex flex-column">
|
||||
{{ template "jam_2024_lj_guidelines.html" . }}
|
||||
<div>
|
||||
<div class="mv5 h3 fill-current link--white">
|
||||
<a href="{{ .Header.HMNHomepageUrl }}">{{ svg "hmn_circuit" }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
|
@ -1,81 +0,0 @@
|
|||
{{ template "jam_2024_learning_base.html" . }}
|
||||
|
||||
{{ define "content-top" }}
|
||||
{{ if or (gt .DaysUntilStart 0) (eq .DaysUntilEnd 0) }}
|
||||
{{ template "jam_2024_lj_bannerbig.html" . }}
|
||||
{{ else }}
|
||||
{{ template "jam_2024_lj_bannersmall.html" . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="mw7 margin-center flex flex-column">
|
||||
{{ if gt .DaysUntilStart 0 }}
|
||||
{{ template "before-jam" . }}
|
||||
{{ else if gt .DaysUntilEnd 0 }}
|
||||
{{ template "during-jam" . }}
|
||||
{{ else }}
|
||||
{{ template "after-jam" . }}
|
||||
{{ end }}
|
||||
|
||||
<div>
|
||||
<div class="mv5 h3 fill-current link--white">
|
||||
<a href="{{ .Header.HMNHomepageUrl }}">{{ svg "hmn_circuit" }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ define "before-jam" }}
|
||||
{{ template "jam_2024_lj_guidelines.html" . }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "during-jam" }}
|
||||
{{ if .Projects.Projects }}
|
||||
{{ template "jam_2024_lj_description.html" . }}
|
||||
{{ if .TimelineItems }}
|
||||
<div class="ph3 pv4 bb b--rich-gray">
|
||||
<h2 class="c--theme-gradient-light">Recent updates</h2>
|
||||
<p>
|
||||
See what community members are learning about their topics. You can share your own updates via your <a href="{{ or .SubmittedProjectUrl .NewProjectUrl }}">project page</a> or using the <b>!til</b> command on Discord.
|
||||
</p>
|
||||
<div class="flex flex-column g2 mt3">
|
||||
{{ template "jam_2024_lj_feeditems.html" .TimelineItems }}
|
||||
<a href="{{ .JamFeedUrl }}" class="link--white tc pa1">
|
||||
See all updates<div class="dib svgicon f8 ml1">{{ svg "chevron-right" }}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="ph3 pv4 bb b--rich-gray">
|
||||
<h2 class="c--theme-gradient-light mb3">Projects</h2>
|
||||
{{ template "jam_2024_lj_projects.html" .Projects }}
|
||||
</div>
|
||||
{{ else }}
|
||||
{{ template "jam_2024_lj_guidelines.html" . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "after-jam" }}
|
||||
{{ template "jam_2024_lj_description.html" . }}
|
||||
<div class="ph3 pv4 bb b--rich-gray">
|
||||
<h2 class="dib c--theme-gradient-light">Recap show</h2>
|
||||
<p>
|
||||
Watch the recap show celebrating all the submissions:
|
||||
</p>
|
||||
<div class="mt3" style="aspect-ratio: 16 / 9;">
|
||||
<iframe width="100%" height="100%" src="https://www.youtube-nocookie.com/embed/MCrt472DkxE?si=PGWFnM8rK4GqCjH7" title="HMN Learning Jam 2024 Recap Stream" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ph3 pv4 bb b--rich-gray">
|
||||
<div class="flex justify-between items-baseline">
|
||||
<h2 class="c--theme-gradient-light mb0">Submitted projects</h2>
|
||||
<a href="{{ .JamFeedUrl }}" class="link--white tc">
|
||||
See all updates<div class="dib svgicon f8 ml1">{{ svg "chevron-right" }}</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt3">
|
||||
{{ template "jam_2024_lj_projects.html" .Projects }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
|
@ -1,333 +0,0 @@
|
|||
{{/*
|
||||
This is a copy-paste from base.html because we want to preserve the unique
|
||||
style of this page no matter what future changes we make to the base.
|
||||
*/}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US" {{ if .OpenGraphItems }} prefix="og: http://ogp.me/ns#"{{ end }}>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ static "favicon-16x16.png" }}">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ static "favicon-32x32.png" }}">
|
||||
|
||||
{{ if .CanonicalLink }}<link rel="canonical" href="{{ .CanonicalLink }}">{{ end }}
|
||||
{{ range .OpenGraphItems }}
|
||||
{{ if .Property }}
|
||||
<meta property="{{ .Property }}" content="{{ .Value }}" />
|
||||
{{ else }}
|
||||
<meta name="{{ .Name }}" content="{{ .Value }}" />
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if .Title }}
|
||||
<title>{{ .Title }} | Handmade Network</title>
|
||||
{{ else }}
|
||||
<title>Handmade Network</title>
|
||||
{{ end }}
|
||||
<meta name="theme-color" content="#232426">
|
||||
|
||||
<!-- <script src="{{ static "js/templates.js" }}"></script> -->
|
||||
|
||||
<link rel="stylesheet" href="{{ static "fonts/mohave/stylesheet.css" }}">
|
||||
<link href='https://fonts.googleapis.com/css?family=Fira+Sans:300,400,500,600' rel='stylesheet' type='text/css'>
|
||||
<link href='https://fonts.googleapis.com/css?family=Fira+Mono:300,400,500,700' rel='stylesheet' type='text/css'>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" type="text/css" href="{{ static "style.css" }}">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--theme-gradient-dark: linear-gradient(to bottom right, #232426, #BFB6BE);
|
||||
--theme-gradient-light: linear-gradient(to bottom right, #8BD5FF, #5899FF);
|
||||
--white: #fff;
|
||||
--bg-button: rgba(255, 255, 255, 0);
|
||||
--bg-button-hover: rgba(255, 255, 255, 0.1);
|
||||
--charcoal: #2F2F2F;
|
||||
--gray: #CBCBCB;
|
||||
--rich-gray: #494949;
|
||||
|
||||
--spacing-0: 0;
|
||||
--spacing-1: .25rem;
|
||||
--spacing-2: .5rem;
|
||||
--spacing-3: 1rem;
|
||||
--spacing-4: 2rem;
|
||||
--spacing-5: 4rem;
|
||||
--spacing-6: 8rem;
|
||||
--spacing-7: 16rem;
|
||||
--border-radius-2: 0.25rem;
|
||||
|
||||
--link-color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Inter", "Fira Sans", sans-serif;
|
||||
font-size: 1rem; /* remove this override when base stylesheet is less dumb */
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.w2-5 {
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.link--white {
|
||||
--link-color: var(--white);
|
||||
}
|
||||
|
||||
.post-content p {
|
||||
/* stupid override, should be done by .post-content instead of .content */
|
||||
margin: 0.6rem 0;
|
||||
}
|
||||
|
||||
.bg--theme-gradient-dark {
|
||||
background: var(--theme-gradient-dark);
|
||||
/* background: linear-gradient(to bottom right, #333, #BBB); */
|
||||
}
|
||||
|
||||
.bg--theme-gradient-light {
|
||||
background: var(--theme-gradient-light);
|
||||
}
|
||||
|
||||
.bg--charcoal {
|
||||
background: var(--charcoal);
|
||||
}
|
||||
|
||||
.bg--rich-gray {
|
||||
background: var(--rich-gray);
|
||||
}
|
||||
|
||||
.bg--gray {
|
||||
background: var(--gray);
|
||||
}
|
||||
|
||||
.b--charcoal {
|
||||
border-color: var(--charcoal);
|
||||
}
|
||||
|
||||
.b--rich-gray {
|
||||
border-color: var(--rich-gray);
|
||||
}
|
||||
|
||||
.c--theme-gradient-dark {
|
||||
background: var(--theme-gradient-dark);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.c--theme-gradient-light {
|
||||
background: var(--theme-gradient-light);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
}
|
||||
|
||||
.c--theme-gradient-light {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.c--gray {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.input--jam {
|
||||
border: 1px solid var(--white);
|
||||
border-radius: var(--border-radius-2) !important;
|
||||
padding: var(--spacing-2) !important;
|
||||
}
|
||||
|
||||
.btn--jam {
|
||||
border: 1px solid var(--white);
|
||||
border-radius: var(--border-radius-2);
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
|
||||
background-color: var(--bg-button);
|
||||
}
|
||||
|
||||
.btn--jam.small {
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
}
|
||||
|
||||
.btn--jam:hover {
|
||||
background-color: var(--bg-button-hover);
|
||||
}
|
||||
|
||||
.button-simple {
|
||||
background: var(--bg-button);
|
||||
border: 1px solid var(--white);
|
||||
}
|
||||
|
||||
.button-simple:hover {
|
||||
background: var(--bg-button-hover);
|
||||
}
|
||||
|
||||
.c-white {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.svg-mask {
|
||||
mask: var(--mask-url);
|
||||
-webkit-mask: var(--mask-url);
|
||||
}
|
||||
|
||||
.fill-current {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.square {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.wide-screen {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.iframe-fill iframe {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.hmn-logo {
|
||||
font-family: 'MohaveHMN', sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.6rem;
|
||||
padding: 0.2rem 0.8rem;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
padding-right: 0.6rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
border-radius: 999px;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.user-avatar.header {
|
||||
width: 1.8rem;
|
||||
}
|
||||
|
||||
.jam-logo {
|
||||
max-width: 18rem;
|
||||
}
|
||||
|
||||
.jam-logo-small {
|
||||
max-width: 8rem;
|
||||
}
|
||||
|
||||
.jam-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
/* align the bounding box with the visible text */
|
||||
margin-left: -0.06em;
|
||||
margin-right: -0.03em;
|
||||
}
|
||||
|
||||
.w-headline {
|
||||
width: 20.3rem;
|
||||
}
|
||||
|
||||
/* not small */
|
||||
@media screen and (min-width: 35em) {
|
||||
.w-headline {
|
||||
width: 44.67rem;
|
||||
}
|
||||
|
||||
.jam-title {
|
||||
font-size: 5.53rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg--theme-gradient-dark c-white flex flex-column">
|
||||
<div class="bg-black-20 flex flex-row items-center link--white">
|
||||
<a href="{{ .Header.HMNHomepageUrl }}" class="hmn-logo flex-shrink-0">
|
||||
Handmade
|
||||
</a>
|
||||
<div class="flex-grow-1 flex-shrink-1"></div>
|
||||
<div class="header-nav flex flex-row items-center g3 lh-solid f6">
|
||||
<a href="{{ .Header.ProjectIndexUrl }}">Projects</a>
|
||||
<a href="{{ .Header.JamsUrl }}">Jams</a>
|
||||
<a class="db" href="{{ or .Header.UserProfileUrl .LoginPageUrl }}">
|
||||
<img class="user-avatar header" src="{{ .UserAvatarUrl }}">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tc flex-grow-1 flex justify-center items-center-ns g5 pv5">
|
||||
<div class="w-headline flex flex-column">
|
||||
<div class="jam-title mb4 lh-solid">Handmade Jams</div>
|
||||
<div class="flex flex-column flex-row-ns justify-between-ns g4 g0-ns lh-solid">
|
||||
<div>
|
||||
<div class="fw7 f3 f2-ns">Visibility Jam</div>
|
||||
<div class="fw3 f4 mt1">July 19-21, 2024</div>
|
||||
<div class="fw6 f5 pt3">
|
||||
<a href="{{ .Visibility2023Url }}" class="db">
|
||||
Last year's jam<div class="dib svgicon f8 ml1">{{ svg "chevron-right" }}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw7 f3 f2-ns">Wheel Reinvention Jam</div>
|
||||
<div class="fw3 f4 mt1">September 23-29, 2024</div>
|
||||
<div class="fw6 f5 pt3">
|
||||
<a href="{{ .WRJ2023Url }}" class="db">
|
||||
Last year's jam<div class="dib svgicon f8 ml1">{{ svg "chevron-right" }}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tl tj-ns mt4 post-content">
|
||||
<p>The Handmade community runs multiple programming jams every year. If you've participated in a game jam before, the idea is similar—an event where participants build something from scratch in a short period of time. Unlike a game jam, though, we don't declare winners (although we do celebrate our favorite results). <a class="fw6" href="{{ .JamsUrl }}">See all our prior jams here.</a></p>
|
||||
<p>To participate in the next one, <a class="fw6" href="https://discord.gg/hmn">join our Discord</a>, or sign up for our mailing list:</p>
|
||||
</div>
|
||||
<form id="newsletter-form" class="mt4 flex flex-column g1">
|
||||
<div class="flex g2 justify-center">
|
||||
<input id="newsletter-email" type="email" class="input--jam flex-grow-1 flex-grow-0-ns" placeholder="me@example.com">
|
||||
<button id="newsletter-button" class="btn--jam">Sign up</button>
|
||||
</div>
|
||||
<div id="newsletter-message" class="f7">
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector("#newsletter-form");
|
||||
const emailField = document.querySelector("#newsletter-email");
|
||||
const button = document.querySelector("#newsletter-button");
|
||||
const message = document.querySelector("#newsletter-message");
|
||||
|
||||
form.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch("{{ .NewsletterSignupUrl }}", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"email": emailField.value,
|
||||
}),
|
||||
});
|
||||
if (res.status >= 400) {
|
||||
throw new Error("bad request");
|
||||
}
|
||||
|
||||
message.innerText = "Signed up successfully!";
|
||||
} catch (err) {
|
||||
message.innerText = "There was an error signing up.";
|
||||
}
|
||||
|
||||
button.disabled = false;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -16,11 +16,6 @@
|
|||
<p>Since 2020, we have been running programming jams to encourage community members to explore new ideas and start projects. You can view all the past submissions and results here.</p>
|
||||
|
||||
<div class="jam-grid g3">
|
||||
<a href="{{ .LJ2024Url }}">
|
||||
<div class="br2 overflow-hidden flex">
|
||||
<img src="{{ static "learningjam2024/2024LJTwitterCard.png" }}">
|
||||
</div>
|
||||
</a>
|
||||
<a href="{{ .WRJ2023Url }}">
|
||||
<div class="br2 overflow-hidden flex">
|
||||
<img src="{{ static "wheeljam2023/TwitterCard.png" }}">
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
{{ define "extrahead" }}
|
||||
<script src="{{ static "js/templates.js" }}"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
|
@ -112,6 +111,73 @@
|
|||
</div>
|
||||
*/}}
|
||||
|
||||
<div class="mb3 ph3 ph0-ns">
|
||||
<style>
|
||||
#jam-banner {
|
||||
background: linear-gradient(174deg, #23CE76, #299CE0);
|
||||
color: white !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#jam-banner h3 {
|
||||
font-family: 'MohaveHMN', sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: 2.2rem;
|
||||
line-height: 0.8;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#jam-details {
|
||||
font-family: 'MohaveHMN', sans-serif;
|
||||
font-variant: small-caps;
|
||||
font-size: 1.2rem;
|
||||
line-height: 0.8;
|
||||
margin-top: 0.6rem;
|
||||
letter-spacing: 0.02rem;
|
||||
}
|
||||
|
||||
#jam-learn-more {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 30rem) {
|
||||
#jam-banner {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#jam-title-container {
|
||||
padding-top: 0.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<a id="jam-banner" class="pv3 ph3 ph4-l br3 flex flex-column flex-row-ns items-center" href="{{ .JamUrl }}">
|
||||
<img class="h3" src="{{ static "wheeljam2023/logo.svg" }}">
|
||||
<div id="jam-title-container" class="flex flex-column pl3-m pl4-l pv3 pv0-ns">
|
||||
<h3 id="jam-title">Wheel Reinvention Jam</h3>
|
||||
<div id="jam-details">
|
||||
September 25 - October 1.
|
||||
{{ if gt .JamDaysUntilEnd 0 }}
|
||||
{{ if eq .JamDaysUntilStart 0 }}
|
||||
<b>Happening now.</b>
|
||||
{{ else if eq .JamDaysUntilStart 1 }}
|
||||
<b>Starting tomorrow.</b>
|
||||
{{ else }}
|
||||
<b>In {{ .JamDaysUntilStart }} days.</b>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<b>See the results.</b>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1"></div>
|
||||
<div id="jam-learn-more">
|
||||
Learn more
|
||||
<div class="dib svgicon">{{ svg "chevron-right" }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mb3 ph3 ph0-ns">
|
||||
<style>
|
||||
#cities-banner {
|
||||
|
@ -189,7 +255,7 @@
|
|||
<div id="cities-title-container" class="flex flex-column pl3-m pl4-l pv3 pv0-ns">
|
||||
<h3 id="cities-title">Handmade cities</h3>
|
||||
<div class="cities-details">
|
||||
Boston - Aug 9-10 2024.
|
||||
Boston - Aug 3-4 2023.
|
||||
{{ if gt .HMBostonDaysUntilEnd 0 }}
|
||||
{{ if eq .HMBostonDaysUntilStart 0 }}
|
||||
<b>Happening now.</b>
|
||||
|
@ -201,7 +267,7 @@
|
|||
{{ end }}
|
||||
</div>
|
||||
<div class="cities-details">
|
||||
Seattle - Nov 20-22 2024.
|
||||
Seattle - Nov 15-17 2023.
|
||||
{{ if gt .HMSDaysUntilEnd 0 }}
|
||||
{{ if eq .HMSDaysUntilStart 0 }}
|
||||
<b>Happening now.</b>
|
||||
|
@ -222,176 +288,6 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mb3 ph3 ph0-ns">
|
||||
<style>
|
||||
#jam-banner {
|
||||
background: linear-gradient(to bottom right, #003c83, #019AD2);
|
||||
color: white !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#jam-banner .jam-logo {
|
||||
height: 7.25rem;
|
||||
}
|
||||
|
||||
#jam-banner h3 {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 2.31rem;
|
||||
/* line-height: 0.8; */
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#jam-title-container {
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
#jam-details {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 1.2rem;
|
||||
/* line-height: 0.8; */
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
#jam-learn-more {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 30rem) {
|
||||
#jam-banner {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#jam-banner h3 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
#jam-banner .jam-logo {
|
||||
height: 5.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<a id="jam-banner" class="pv3 ph3 ph4-l br3 flex flex-column flex-row-ns items-center" href="{{ .JamUrl }}">
|
||||
<img class="jam-logo" src="{{ static "learningjam2024/logo.svg" }}">
|
||||
<div id="jam-title-container" class="flex flex-column pl3-m pl4-l pv3 pv0-ns">
|
||||
<h3 id="jam-title">Learning Jam</h3>
|
||||
<div id="jam-details">
|
||||
<div class="db dn-ns">
|
||||
<div>
|
||||
March 15-17. March 22-24.
|
||||
</div>
|
||||
<div>
|
||||
{{ if gt .JamDaysUntilEnd 0 }}
|
||||
{{ if eq .JamDaysUntilStart 0 }}
|
||||
<b>Happening now.</b>
|
||||
{{ else if eq .JamDaysUntilStart 1 }}
|
||||
<b>Starting tomorrow.</b>
|
||||
{{ else }}
|
||||
<b>In {{ .JamDaysUntilStart }} days.</b>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<b>See the results.</b>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dn db-ns">
|
||||
<div>
|
||||
March 15-17.
|
||||
</div>
|
||||
<div>
|
||||
March 22-24.
|
||||
{{ if gt .JamDaysUntilEnd 0 }}
|
||||
{{ if eq .JamDaysUntilStart 0 }}
|
||||
<b>Happening now.</b>
|
||||
{{ else if eq .JamDaysUntilStart 1 }}
|
||||
<b>Starting tomorrow.</b>
|
||||
{{ else }}
|
||||
<b>In {{ .JamDaysUntilStart }} days.</b>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<b>See the results.</b>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1"></div>
|
||||
<div id="jam-learn-more">
|
||||
Learn more
|
||||
<div class="dib svgicon">{{ svg "chevron-right" }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{/*
|
||||
<div class="mb3 ph3 ph0-ns">
|
||||
<style>
|
||||
#jam-banner {
|
||||
background: linear-gradient(174deg, #23CE76, #299CE0);
|
||||
color: white !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#jam-banner h3 {
|
||||
font-family: 'MohaveHMN', sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: 2.2rem;
|
||||
line-height: 0.8;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#jam-details {
|
||||
font-family: 'MohaveHMN', sans-serif;
|
||||
font-variant: small-caps;
|
||||
font-size: 1.2rem;
|
||||
line-height: 0.8;
|
||||
margin-top: 0.6rem;
|
||||
letter-spacing: 0.02rem;
|
||||
}
|
||||
|
||||
#jam-learn-more {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 30rem) {
|
||||
#jam-banner {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#jam-title-container {
|
||||
padding-top: 0.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<a id="jam-banner" class="pv3 ph3 ph4-l br3 flex flex-column flex-row-ns items-center" href="{{ .JamUrl }}">
|
||||
<img class="h3" src="{{ static "wheeljam2023/logo.svg" }}">
|
||||
<div id="jam-title-container" class="flex flex-column pl3-m pl4-l pv3 pv0-ns">
|
||||
<h3 id="jam-title">Wheel Reinvention Jam</h3>
|
||||
<div id="jam-details">
|
||||
September 25 - October 1.
|
||||
{{ if gt .JamDaysUntilEnd 0 }}
|
||||
{{ if eq .JamDaysUntilStart 0 }}
|
||||
<b>Happening now.</b>
|
||||
{{ else if eq .JamDaysUntilStart 1 }}
|
||||
<b>Starting tomorrow.</b>
|
||||
{{ else }}
|
||||
<b>In {{ .JamDaysUntilStart }} days.</b>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<b>See the results.</b>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1"></div>
|
||||
<div id="jam-learn-more">
|
||||
Learn more
|
||||
<div class="dib svgicon">{{ svg "chevron-right" }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
*/}}
|
||||
|
||||
{{ if not .User }}
|
||||
<div class="mb3 ph3 ph0-ns">
|
||||
<style>
|
||||
|
|
|
@ -1,293 +0,0 @@
|
|||
{{/*
|
||||
This is a copy-paste from base.html because we want to preserve the unique
|
||||
style of this page no matter what future changes we make to the base.
|
||||
*/}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US" {{ if .OpenGraphItems }} prefix="og: http://ogp.me/ns#"{{ end }}>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ static "learningjam2024/favicon-16x16.png" }}">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ static "learningjam2024/favicon-32x32.png" }}">
|
||||
|
||||
{{ if .CanonicalLink }}<link rel="canonical" href="{{ .CanonicalLink }}">{{ end }}
|
||||
{{ range .OpenGraphItems }}
|
||||
{{ if .Property }}
|
||||
<meta property="{{ .Property }}" content="{{ .Value }}" />
|
||||
{{ else }}
|
||||
<meta name="{{ .Name }}" content="{{ .Value }}" />
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if .Title }}
|
||||
<title>{{ .Title }} | Handmade Network</title>
|
||||
{{ else }}
|
||||
<title>Handmade Network</title>
|
||||
{{ end }}
|
||||
<meta name="theme-color" content="#003C83">
|
||||
|
||||
<script src="{{ static "js/templates.js" }}"></script>
|
||||
|
||||
<link rel="stylesheet" href="{{ static "fonts/mohave/stylesheet.css" }}">
|
||||
<link href='https://fonts.googleapis.com/css?family=Fira+Sans:300,400,500,600' rel='stylesheet' type='text/css'>
|
||||
<link href='https://fonts.googleapis.com/css?family=Fira+Mono:300,400,500,700' rel='stylesheet' type='text/css'>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" type="text/css" href="{{ static "style.css" }}">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--theme-gradient-dark: linear-gradient(to bottom right, #003c83, #019AD2);
|
||||
--theme-gradient-light: linear-gradient(to bottom right, #8BD5FF, #5899FF);
|
||||
--white: #fff;
|
||||
--bg-button: rgba(255, 255, 255, 0);
|
||||
--bg-button-hover: rgba(255, 255, 255, 0.1);
|
||||
--charcoal: #2F2F2F;
|
||||
--gray: #CBCBCB;
|
||||
--rich-gray: #494949;
|
||||
|
||||
--spacing-0: 0;
|
||||
--spacing-1: .25rem;
|
||||
--spacing-2: .5rem;
|
||||
--spacing-3: 1rem;
|
||||
--spacing-4: 2rem;
|
||||
--spacing-5: 4rem;
|
||||
--spacing-6: 8rem;
|
||||
--spacing-7: 16rem;
|
||||
--border-radius-2: 0.25rem;
|
||||
|
||||
--link-color: transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Inter", "Fira Sans", sans-serif;
|
||||
font-size: 1rem; /* remove this override when base stylesheet is less dumb */
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.w2-5 {
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.link--white {
|
||||
--link-color: var(--white);
|
||||
}
|
||||
|
||||
.post-content p {
|
||||
/* stupid override, should be done by .post-content instead of .content */
|
||||
margin: 0.6rem 0;
|
||||
}
|
||||
|
||||
.bg--theme-gradient-dark {
|
||||
background: var(--theme-gradient-dark);
|
||||
}
|
||||
|
||||
.bg--theme-gradient-light {
|
||||
background: var(--theme-gradient-light);
|
||||
}
|
||||
|
||||
.bg--charcoal {
|
||||
background: var(--charcoal);
|
||||
}
|
||||
|
||||
.bg--rich-gray {
|
||||
background: var(--rich-gray);
|
||||
}
|
||||
|
||||
.bg--gray {
|
||||
background: var(--gray);
|
||||
}
|
||||
|
||||
.b--charcoal {
|
||||
border-color: var(--charcoal);
|
||||
}
|
||||
|
||||
.b--rich-gray {
|
||||
border-color: var(--rich-gray);
|
||||
}
|
||||
|
||||
.c--theme-gradient-dark {
|
||||
background: var(--theme-gradient-dark);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
a, .c--theme-gradient-light {
|
||||
background: var(--theme-gradient-light);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
}
|
||||
|
||||
.c--theme-gradient-light {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.c--gray {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.btn--jam {
|
||||
border: 1px solid var(--white);
|
||||
border-radius: var(--border-radius-2);
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
|
||||
background-color: var(--bg-button);
|
||||
}
|
||||
|
||||
.btn--jam.small {
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
}
|
||||
|
||||
.btn--jam:hover {
|
||||
background-color: var(--bg-button-hover);
|
||||
}
|
||||
|
||||
.button-simple {
|
||||
background: var(--bg-button);
|
||||
border: 1px solid var(--white);
|
||||
}
|
||||
|
||||
.button-simple:hover {
|
||||
background: var(--bg-button-hover);
|
||||
}
|
||||
|
||||
.c-white {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.svg-mask {
|
||||
mask: var(--mask-url);
|
||||
-webkit-mask: var(--mask-url);
|
||||
}
|
||||
|
||||
.fill-current {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.square {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.wide-screen {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.iframe-fill iframe {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.hmn-logo {
|
||||
font-family: 'MohaveHMN', sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.6rem;
|
||||
padding: 0.2rem 0.8rem;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
padding-right: 0.6rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
border-radius: 999px;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.user-avatar.header {
|
||||
width: 1.8rem;
|
||||
}
|
||||
|
||||
.jam-logo {
|
||||
max-width: 18rem;
|
||||
}
|
||||
|
||||
.jam-logo-small {
|
||||
max-width: 8rem;
|
||||
}
|
||||
|
||||
.jam-title {
|
||||
/* align with the width of the logo */
|
||||
font-size: 2.76rem;
|
||||
font-weight: 700;
|
||||
/* align with the text's actual bounding box */
|
||||
line-height: 1.18;
|
||||
margin-top: -0.18em;
|
||||
margin-left: -0.03em;
|
||||
margin-right: -0.03em;
|
||||
}
|
||||
|
||||
.jam-title.small {
|
||||
font-size: 2.25rem;
|
||||
/* align with the text's actual bounding box */
|
||||
line-height: 1.18;
|
||||
margin-top: -0.18em;
|
||||
margin-left: -0.03em;
|
||||
margin-right: -0.03em;
|
||||
}
|
||||
|
||||
/* not small */
|
||||
@media screen and (min-width: 35em) {
|
||||
.jam-logo {
|
||||
max-width: 24rem;
|
||||
}
|
||||
|
||||
.jam-title {
|
||||
font-size: 3.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* not small */
|
||||
@media screen and (min-width: 35em) {
|
||||
.flex-basis-40-ns {
|
||||
flex-basis: 40%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{{ template "extrahead" . }}
|
||||
</head>
|
||||
|
||||
<body class="bg--charcoal flex flex-column">
|
||||
<div class="c-white bg--theme-gradient-dark">
|
||||
<div class="bg-black-20 flex flex-row items-center link--white">
|
||||
<a href="{{ .Header.HMNHomepageUrl }}" class="hmn-logo flex-shrink-0">
|
||||
Handmade
|
||||
</a>
|
||||
<div class="flex-grow-1 flex-shrink-1"></div>
|
||||
<div class="header-nav flex flex-row items-center g3 lh-solid f6">
|
||||
<a href="{{ .Header.ProjectIndexUrl }}">Projects</a>
|
||||
<a href="{{ .Header.JamsUrl }}">Jams</a>
|
||||
<a class="db" href="{{ or .Header.UserProfileUrl .LoginPageUrl }}">
|
||||
<img class="user-avatar header" src="{{ .UserAvatarUrl }}">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ block "content-top" . }}{{ end }}
|
||||
</div>
|
||||
<div class="bg--charcoal c-white flex-grow-1">
|
||||
{{ block "content" . }}{{ end }}
|
||||
</div>
|
||||
<script>
|
||||
const dtf = new Intl.DateTimeFormat([], {
|
||||
dateStyle: "full",
|
||||
timeStyle: "short",
|
||||
});
|
||||
|
||||
for (const time of document.querySelectorAll('time')) {
|
||||
const d = new Date(Date.parse(time.dateTime));
|
||||
if (time.getAttribute("data-type") == "content") {
|
||||
time.textContent = dtf.format(d);
|
||||
} else {
|
||||
time.title = dtf.format(d);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -132,8 +132,6 @@
|
|||
2023 Wheel Reinvention Jam
|
||||
{{- else if eq . "visibility-2023" -}}
|
||||
2023 Visibility Jam
|
||||
{{- else if eq . "learning-2024" -}}
|
||||
2024 Learning Jam
|
||||
{{- else -}}
|
||||
???
|
||||
{{- end -}}
|
||||
|
|
|
@ -252,10 +252,6 @@ var HMNTemplateFuncs = template.FuncMap{
|
|||
iso := t.UTC().Format(time.RFC3339)
|
||||
return template.HTML(fmt.Sprintf(`<time datetime="%s">%s</time>`, iso, formatted))
|
||||
},
|
||||
"timehtmlcontent": func(t time.Time) template.HTML {
|
||||
iso := t.UTC().Format(time.RFC3339)
|
||||
return template.HTML(fmt.Sprintf(`<time data-type="content" datetime="%s"></time>`, iso))
|
||||
},
|
||||
"noescape": func(str string) template.HTML {
|
||||
return template.HTML(str)
|
||||
},
|
||||
|
@ -287,12 +283,6 @@ var HMNTemplateFuncs = template.FuncMap{
|
|||
"trim": func(str template.HTML) template.HTML {
|
||||
return template.HTML(strings.TrimSpace(string(str)))
|
||||
},
|
||||
"lastidx": func(idx int, l int) bool {
|
||||
return idx == l-1
|
||||
},
|
||||
"isodd": func(num int) bool {
|
||||
return num%2 == 1
|
||||
},
|
||||
|
||||
// NOTE(asaf): Template specific functions:
|
||||
"projectcarddata": func(project Project, classes string) ProjectCardData {
|
||||
|
|
|
@ -16,12 +16,10 @@ type BaseData struct {
|
|||
Notices []Notice
|
||||
ReportIssueEmail string
|
||||
|
||||
CurrentUrl string
|
||||
CurrentProjectUrl string
|
||||
LoginPageUrl string
|
||||
ProjectCSSUrl string
|
||||
DiscordInviteUrl string
|
||||
NewsletterSignupUrl string
|
||||
CurrentUrl string
|
||||
CurrentProjectUrl string
|
||||
LoginPageUrl string
|
||||
ProjectCSSUrl string
|
||||
|
||||
Project Project
|
||||
User *User
|
||||
|
@ -57,7 +55,6 @@ type Header struct {
|
|||
ConferencesUrl string
|
||||
JamsUrl string
|
||||
EducationUrl string
|
||||
CalendarUrl string
|
||||
|
||||
Project *ProjectHeader
|
||||
}
|
||||
|
@ -418,11 +415,3 @@ type EduArticle struct {
|
|||
|
||||
Content template.HTML
|
||||
}
|
||||
|
||||
type CalendarEvent struct {
|
||||
Name string
|
||||
Desc string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
CalName string
|
||||
}
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
package website
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/db"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
)
|
||||
|
||||
func APICheckUsername(c *RequestContext) ResponseData {
|
||||
|
@ -47,59 +44,12 @@ func APICheckUsername(c *RequestContext) ResponseData {
|
|||
}
|
||||
|
||||
var res ResponseData
|
||||
res.Header().Set("Content-Type", "application/json")
|
||||
addCORSHeaders(c, &res)
|
||||
if found {
|
||||
res.WriteJson(map[string]any{
|
||||
"found": true,
|
||||
"canonical": canonicalUsername,
|
||||
}, nil)
|
||||
res.Write([]byte(fmt.Sprintf(`{ "found": true, "canonical": "%s" }`, canonicalUsername)))
|
||||
} else {
|
||||
res.WriteJson(map[string]any{
|
||||
"found": false,
|
||||
}, nil)
|
||||
res.Write([]byte(`{ "found": false }`))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func APINewsletterSignup(c *RequestContext) ResponseData {
|
||||
bodyBytes := utils.Must1(io.ReadAll(c.Req.Body))
|
||||
type Input struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
var input Input
|
||||
err := json.Unmarshal(bodyBytes, &input)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
var res ResponseData
|
||||
|
||||
sanitized := input.Email
|
||||
sanitized = strings.TrimSpace(sanitized)
|
||||
sanitized = strings.ToLower(sanitized)
|
||||
if len(sanitized) > 200 {
|
||||
res.StatusCode = http.StatusBadRequest
|
||||
return res
|
||||
}
|
||||
if !strings.Contains(sanitized, "@") {
|
||||
res.StatusCode = http.StatusBadRequest
|
||||
res.WriteJson(map[string]any{
|
||||
"error": "bad email",
|
||||
}, nil)
|
||||
return res
|
||||
}
|
||||
|
||||
_, err = c.Conn.Exec(c,
|
||||
`
|
||||
INSERT INTO newsletter_emails (email) VALUES ($1)
|
||||
ON CONFLICT DO NOTHING
|
||||
`,
|
||||
sanitized,
|
||||
)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to save email into database"))
|
||||
}
|
||||
|
||||
res.WriteHeader(http.StatusNoContent)
|
||||
return res
|
||||
}
|
||||
|
|
|
@ -47,12 +47,10 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
|
|||
Title: title,
|
||||
Breadcrumbs: breadcrumbs,
|
||||
|
||||
CurrentUrl: c.FullUrl(),
|
||||
CurrentProjectUrl: c.UrlContext.BuildHomepage(),
|
||||
LoginPageUrl: hmnurl.BuildLoginPage(c.FullUrl()),
|
||||
ProjectCSSUrl: hmnurl.BuildProjectCSS(project.Color1),
|
||||
DiscordInviteUrl: "https://discord.gg/hmn",
|
||||
NewsletterSignupUrl: hmnurl.BuildAPINewsletterSignup(),
|
||||
CurrentUrl: c.FullUrl(),
|
||||
CurrentProjectUrl: c.UrlContext.BuildHomepage(),
|
||||
LoginPageUrl: hmnurl.BuildLoginPage(c.FullUrl()),
|
||||
ProjectCSSUrl: hmnurl.BuildProjectCSS(project.Color1),
|
||||
|
||||
Project: templates.ProjectToTemplate(&project, c.UrlContext.BuildHomepage()),
|
||||
User: templateUser,
|
||||
|
@ -81,7 +79,6 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
|
|||
ConferencesUrl: hmnurl.BuildConferences(),
|
||||
JamsUrl: hmnurl.BuildJamsIndex(),
|
||||
EducationUrl: hmnurl.BuildEducationIndex(),
|
||||
CalendarUrl: hmnurl.BuildCalendarIndex(),
|
||||
},
|
||||
Footer: templates.Footer{
|
||||
HomepageUrl: hmnurl.BuildHomepage(),
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
package website
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/calendar"
|
||||
"git.handmade.network/hmn/hmn/src/config"
|
||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||
"git.handmade.network/hmn/hmn/src/templates"
|
||||
)
|
||||
|
||||
func CalendarIndex(c *RequestContext) ResponseData {
|
||||
type CalData struct {
|
||||
templates.BaseData
|
||||
Calendars []string
|
||||
Events []templates.CalendarEvent
|
||||
BaseICalUrl string
|
||||
}
|
||||
events := calendar.GetFutureEvents()
|
||||
|
||||
templateEvents := make([]templates.CalendarEvent, 0, len(events))
|
||||
for _, ev := range events {
|
||||
templateEvents = append(templateEvents, templates.CalendarEventToTemplate(&ev))
|
||||
}
|
||||
|
||||
calNames := []string{}
|
||||
for _, c := range config.Config.Calendars {
|
||||
calNames = append(calNames, c.Name)
|
||||
}
|
||||
|
||||
calendarData := CalData{
|
||||
BaseData: getBaseDataAutocrumb(c, "Calendar"),
|
||||
Calendars: calNames,
|
||||
Events: templateEvents,
|
||||
BaseICalUrl: hmnurl.BuildCalendarICal(),
|
||||
}
|
||||
var res ResponseData
|
||||
res.MustWriteTemplate("calendar_index.html", calendarData, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
||||
func CalendarICal(c *RequestContext) ResponseData {
|
||||
query := c.Req.URL.Query()
|
||||
cals := make([]string, 0, len(query))
|
||||
for key := range query {
|
||||
cals = append(cals, key)
|
||||
}
|
||||
calBytes, err := calendar.GetICal(cals)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, err)
|
||||
}
|
||||
var res ResponseData
|
||||
res.Write(calBytes)
|
||||
return res
|
||||
}
|
|
@ -15,7 +15,6 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/templates"
|
||||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
@ -429,18 +428,3 @@ func saveDiscordAvatar(ctx context.Context, conn db.ConnOrTx, userID, avatarHash
|
|||
|
||||
return asset, nil
|
||||
}
|
||||
|
||||
func DiscordBotDebugPage(c *RequestContext) ResponseData {
|
||||
type DiscordBotDebugData struct {
|
||||
templates.BaseData
|
||||
BotEvents []discord.BotEvent
|
||||
}
|
||||
botEvents := discord.GetBotEvents()
|
||||
var res ResponseData
|
||||
res.MustWriteTemplate("discord_bot_debug.html", DiscordBotDebugData{
|
||||
BaseData: getBaseData(c, "", nil),
|
||||
|
||||
BotEvents: botEvents,
|
||||
}, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
package website
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/config"
|
||||
"git.handmade.network/hmn/hmn/src/db"
|
||||
"git.handmade.network/hmn/hmn/src/hmndata"
|
||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/templates"
|
||||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
|
@ -27,7 +23,6 @@ func JamsIndex(c *RequestContext) ResponseData {
|
|||
WRJ2022Url string
|
||||
VJ2023Url string
|
||||
WRJ2023Url string
|
||||
LJ2024Url string
|
||||
}
|
||||
|
||||
res.MustWriteTemplate("jams_index.html", TemplateData{
|
||||
|
@ -38,279 +33,10 @@ func JamsIndex(c *RequestContext) ResponseData {
|
|||
WRJ2022Url: hmnurl.BuildJamIndex2022(),
|
||||
VJ2023Url: hmnurl.BuildJamIndex2023_Visibility(),
|
||||
WRJ2023Url: hmnurl.BuildJamIndex2023(),
|
||||
LJ2024Url: hmnurl.BuildJamIndex2024_Learning(),
|
||||
}, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
||||
func JamSaveTheDate(c *RequestContext) ResponseData {
|
||||
var res ResponseData
|
||||
|
||||
type TemplateData struct {
|
||||
templates.BaseData
|
||||
UserAvatarUrl string
|
||||
JamsUrl string
|
||||
Visibility2023Url string
|
||||
WRJ2023Url string
|
||||
NewsletterSignupUrl string
|
||||
}
|
||||
|
||||
tmpl := TemplateData{
|
||||
BaseData: getBaseDataAutocrumb(c, "Upcoming Jams"),
|
||||
UserAvatarUrl: templates.UserAvatarUrl(c.CurrentUser, "dark"),
|
||||
JamsUrl: hmnurl.BuildJamsIndex(),
|
||||
Visibility2023Url: hmnurl.BuildJamIndex2023_Visibility(),
|
||||
WRJ2023Url: hmnurl.BuildJamIndex2023(),
|
||||
NewsletterSignupUrl: hmnurl.BuildAPINewsletterSignup(),
|
||||
}
|
||||
tmpl.OpenGraphItems = []templates.OpenGraphItem{
|
||||
{Property: "og:title", Value: "Upcoming Jams"},
|
||||
{Property: "og:site_name", Value: "Handmade Network"},
|
||||
{Property: "og:type", Value: "website"},
|
||||
{Property: "og:image", Value: hmnurl.BuildPublic("HMNLogo_SaveTheDate.png", true)},
|
||||
{Property: "og:description", Value: "Upcoming programming jams from the Handmade community."},
|
||||
{Property: "og:url", Value: hmnurl.BuildJamSaveTheDate()},
|
||||
}
|
||||
|
||||
res.MustWriteTemplate("jam_save_the_date.html", tmpl, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
||||
func JamIndex2024_Learning(c *RequestContext) ResponseData {
|
||||
var res ResponseData
|
||||
|
||||
baseData := getBaseDataAutocrumb(c, hmndata.LJ2024.Name)
|
||||
baseData.OpenGraphItems = opengraphLJ2024
|
||||
|
||||
jamBaseData, err := getLJ2024BaseData(c)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
feedData, err := getLJ2024FeedData(c, 5)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
type JamPageData struct {
|
||||
templates.BaseData
|
||||
JamBaseDataLJ2024
|
||||
TwitchEmbedUrl string
|
||||
|
||||
Projects JamProjectDataLJ2024
|
||||
TimelineItems []templates.TimelineItem
|
||||
}
|
||||
|
||||
tmpl := JamPageData{
|
||||
BaseData: baseData,
|
||||
JamBaseDataLJ2024: jamBaseData,
|
||||
TwitchEmbedUrl: getTwitchEmbedUrl(c),
|
||||
|
||||
Projects: feedData.Projects,
|
||||
TimelineItems: feedData.TimelineItems,
|
||||
}
|
||||
|
||||
res.MustWriteTemplate("jam_2024_lj_index.html", tmpl, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
||||
func JamFeed2024_Learning(c *RequestContext) ResponseData {
|
||||
baseData := getBaseDataAutocrumb(c, hmndata.LJ2024.Name)
|
||||
baseData.OpenGraphItems = opengraphLJ2024
|
||||
|
||||
jamBaseData, err := getLJ2024BaseData(c)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
feedData, err := getLJ2024FeedData(c, 0)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
type JamFeedData struct {
|
||||
templates.BaseData
|
||||
JamBaseDataLJ2024
|
||||
|
||||
Projects JamProjectDataLJ2024
|
||||
TimelineItems []templates.TimelineItem
|
||||
}
|
||||
|
||||
tmpl := JamFeedData{
|
||||
BaseData: baseData,
|
||||
JamBaseDataLJ2024: jamBaseData,
|
||||
|
||||
Projects: feedData.Projects,
|
||||
TimelineItems: feedData.TimelineItems,
|
||||
}
|
||||
|
||||
var res ResponseData
|
||||
res.MustWriteTemplate("jam_2024_lj_feed.html", tmpl, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
||||
func JamGuidelines2024_Learning(c *RequestContext) ResponseData {
|
||||
baseData := getBaseDataAutocrumb(c, hmndata.LJ2024.Name)
|
||||
baseData.OpenGraphItems = opengraphLJ2024
|
||||
|
||||
jamBaseData, err := getLJ2024BaseData(c)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
type JamGuidelinesData struct {
|
||||
templates.BaseData
|
||||
JamBaseDataLJ2024
|
||||
}
|
||||
|
||||
tmpl := JamGuidelinesData{
|
||||
BaseData: baseData,
|
||||
JamBaseDataLJ2024: jamBaseData,
|
||||
}
|
||||
|
||||
var res ResponseData
|
||||
res.MustWriteTemplate("jam_2024_lj_guidelines_index.html", tmpl, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
||||
var opengraphLJ2024 = []templates.OpenGraphItem{
|
||||
{Property: "og:title", Value: "Learning Jam"},
|
||||
{Property: "og:site_name", Value: "Handmade Network"},
|
||||
{Property: "og:type", Value: "website"},
|
||||
{Property: "og:image", Value: hmnurl.BuildPublic("learningjam2024/2024LJOpenGraph.png", true)},
|
||||
{Property: "og:description", Value: "A two-weekend jam where you dive deep into a topic, then share it with the rest of the community."},
|
||||
{Property: "og:url", Value: hmnurl.BuildJamIndex2024_Learning()},
|
||||
{Name: "twitter:card", Value: "summary_large_image"},
|
||||
{Name: "twitter:image", Value: hmnurl.BuildPublic("learningjam2024/2024LJTwitterCard.png", true)},
|
||||
}
|
||||
|
||||
type JamBaseDataLJ2024 struct {
|
||||
UserAvatarUrl string
|
||||
DaysUntilStart, DaysUntilEnd int
|
||||
JamUrl string
|
||||
JamFeedUrl string
|
||||
NewProjectUrl string
|
||||
SubmittedProjectUrl string
|
||||
GuidelinesUrl string
|
||||
}
|
||||
|
||||
type JamProjectDataLJ2024 struct {
|
||||
Projects []templates.Project
|
||||
NewProjectUrl string
|
||||
}
|
||||
|
||||
type JamFeedDataLJ2024 struct {
|
||||
Projects JamProjectDataLJ2024
|
||||
TimelineItems []templates.TimelineItem
|
||||
|
||||
projects []hmndata.ProjectAndStuff
|
||||
}
|
||||
|
||||
func getLJ2024BaseData(c *RequestContext) (JamBaseDataLJ2024, error) {
|
||||
daysUntilStart := daysUntil(hmndata.LJ2024.StartTime)
|
||||
daysUntilEnd := daysUntil(hmndata.LJ2024.EndTime)
|
||||
|
||||
tmpl := JamBaseDataLJ2024{
|
||||
UserAvatarUrl: templates.UserAvatarDefaultUrl("dark"),
|
||||
DaysUntilStart: daysUntilStart,
|
||||
DaysUntilEnd: daysUntilEnd,
|
||||
JamUrl: hmnurl.BuildJamIndex2024_Learning(),
|
||||
JamFeedUrl: hmnurl.BuildJamFeed2024_Learning(),
|
||||
NewProjectUrl: hmnurl.BuildProjectNewJam(),
|
||||
GuidelinesUrl: hmnurl.BuildJamGuidelines2024_Learning(),
|
||||
}
|
||||
|
||||
if c.CurrentUser != nil {
|
||||
tmpl.UserAvatarUrl = templates.UserAvatarUrl(c.CurrentUser, "dark")
|
||||
projects, err := hmndata.FetchProjects(c, c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
|
||||
OwnerIDs: []int{c.CurrentUser.ID},
|
||||
JamSlugs: []string{hmndata.LJ2024.Slug},
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return JamBaseDataLJ2024{}, oops.New(err, "failed to fetch jam projects for current user")
|
||||
}
|
||||
if len(projects) > 0 {
|
||||
urlContext := hmndata.UrlContextForProject(&projects[0].Project)
|
||||
tmpl.SubmittedProjectUrl = urlContext.BuildHomepage()
|
||||
}
|
||||
}
|
||||
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
// 0 for no limit on timeline items.
|
||||
func getLJ2024FeedData(c *RequestContext, maxTimelineItems int) (JamFeedDataLJ2024, error) {
|
||||
jamProjects, err := hmndata.FetchProjects(c, c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
|
||||
JamSlugs: []string{hmndata.LJ2024.Slug},
|
||||
})
|
||||
if err != nil {
|
||||
return JamFeedDataLJ2024{}, oops.New(err, "failed to fetch jam projects for current user")
|
||||
}
|
||||
|
||||
projects := make([]templates.Project, 0, len(jamProjects))
|
||||
for _, jp := range jamProjects {
|
||||
urlContext := hmndata.UrlContextForProject(&jp.Project)
|
||||
projectUrl := urlContext.BuildHomepage()
|
||||
projects = append(projects, templates.ProjectAndStuffToTemplate(&jp, projectUrl, c.Theme))
|
||||
}
|
||||
|
||||
projectIds := make([]int, 0, len(jamProjects))
|
||||
for _, jp := range jamProjects {
|
||||
projectIds = append(projectIds, jp.Project.ID)
|
||||
}
|
||||
|
||||
var timelineItems []templates.TimelineItem
|
||||
if len(projectIds) > 0 {
|
||||
snippets, err := hmndata.FetchSnippets(c, c.Conn, c.CurrentUser, hmndata.SnippetQuery{
|
||||
ProjectIDs: projectIds,
|
||||
Limit: maxTimelineItems,
|
||||
})
|
||||
if err != nil {
|
||||
return JamFeedDataLJ2024{}, oops.New(err, "failed to fetch snippets for jam showcase")
|
||||
}
|
||||
|
||||
timelineItems = make([]templates.TimelineItem, 0, len(snippets))
|
||||
for _, s := range snippets {
|
||||
timelineItem := SnippetToTimelineItem(&s.Snippet, s.Asset, s.DiscordMessage, s.Projects, s.Owner, c.Theme, false)
|
||||
timelineItem.SmallInfo = true
|
||||
timelineItems = append(timelineItems, timelineItem)
|
||||
}
|
||||
}
|
||||
|
||||
return JamFeedDataLJ2024{
|
||||
Projects: JamProjectDataLJ2024{
|
||||
Projects: projects,
|
||||
NewProjectUrl: hmnurl.BuildProjectNewJam(),
|
||||
},
|
||||
TimelineItems: timelineItems,
|
||||
|
||||
projects: jamProjects,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getTwitchEmbedUrl(c *RequestContext) string {
|
||||
twitchEmbedUrl := ""
|
||||
twitchStatus, err := db.QueryOne[models.TwitchLatestStatus](c, c.Conn,
|
||||
`
|
||||
SELECT $columns
|
||||
FROM twitch_latest_status
|
||||
WHERE twitch_login = $1
|
||||
`,
|
||||
"handmadenetwork",
|
||||
)
|
||||
if err != nil {
|
||||
c.Logger.Warn().Err(err).Msg("failed to query Twitch status for the HMN account")
|
||||
} else if twitchStatus.Live {
|
||||
hmnUrl, err := url.Parse(config.Config.BaseUrl)
|
||||
if err == nil {
|
||||
twitchEmbedUrl = fmt.Sprintf("https://player.twitch.tv/?channel=%s&parent=%s", twitchStatus.TwitchLogin, hmnUrl.Hostname())
|
||||
}
|
||||
}
|
||||
|
||||
return twitchEmbedUrl
|
||||
}
|
||||
|
||||
func JamIndex2023(c *RequestContext) ResponseData {
|
||||
var res ResponseData
|
||||
|
||||
|
@ -335,7 +61,6 @@ func JamIndex2023(c *RequestContext) ResponseData {
|
|||
ProjectSubmissionUrl string
|
||||
ShowcaseFeedUrl string
|
||||
ShowcaseJson string
|
||||
TwitchEmbedUrl string
|
||||
|
||||
JamProjects []templates.Project
|
||||
}
|
||||
|
@ -394,24 +119,6 @@ func JamIndex2023(c *RequestContext) ResponseData {
|
|||
|
||||
showcaseJson := templates.TimelineItemsToJSON(showcaseItems)
|
||||
|
||||
twitchEmbedUrl := ""
|
||||
twitchStatus, err := db.QueryOne[models.TwitchLatestStatus](c, c.Conn,
|
||||
`
|
||||
SELECT $columns
|
||||
FROM twitch_latest_status
|
||||
WHERE twitch_login = $1
|
||||
`,
|
||||
"handmadenetwork",
|
||||
)
|
||||
if err == nil {
|
||||
if twitchStatus.Live {
|
||||
hmnUrl, err := url.Parse(config.Config.BaseUrl)
|
||||
if err == nil {
|
||||
twitchEmbedUrl = fmt.Sprintf("https://player.twitch.tv/?channel=%s&parent=%s", twitchStatus.TwitchLogin, hmnUrl.Hostname())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.MustWriteTemplate("jam_2023_wrj_index.html", JamPageData{
|
||||
BaseData: baseData,
|
||||
DaysUntilStart: daysUntilStart,
|
||||
|
@ -423,7 +130,6 @@ func JamIndex2023(c *RequestContext) ResponseData {
|
|||
ShowcaseFeedUrl: hmnurl.BuildJamFeed2023(),
|
||||
ShowcaseJson: showcaseJson,
|
||||
JamProjects: pageProjects,
|
||||
TwitchEmbedUrl: twitchEmbedUrl,
|
||||
}, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ type LandingTemplateData struct {
|
|||
ManifestoUrl string
|
||||
FeedUrl string
|
||||
PodcastUrl string
|
||||
StreamsUrl string
|
||||
ShowcaseUrl string
|
||||
AtomFeedUrl string
|
||||
MarkAllReadUrl string
|
||||
|
@ -150,19 +151,20 @@ func Index(c *RequestContext) ResponseData {
|
|||
ManifestoUrl: hmnurl.BuildManifesto(),
|
||||
FeedUrl: hmnurl.BuildFeed(),
|
||||
PodcastUrl: hmnurl.BuildPodcast(),
|
||||
StreamsUrl: hmnurl.BuildStreams(),
|
||||
ShowcaseUrl: hmnurl.BuildShowcase(),
|
||||
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
||||
MarkAllReadUrl: hmnurl.HMNProjectContext.BuildForumMarkRead(0),
|
||||
|
||||
JamUrl: hmnurl.BuildJamIndex2024_Learning(),
|
||||
JamDaysUntilStart: daysUntil(hmndata.LJ2024.StartTime),
|
||||
JamDaysUntilEnd: daysUntil(hmndata.LJ2024.EndTime),
|
||||
JamUrl: hmnurl.BuildJamIndex2023(),
|
||||
JamDaysUntilStart: daysUntil(hmndata.WRJ2023.StartTime),
|
||||
JamDaysUntilEnd: daysUntil(hmndata.WRJ2023.EndTime),
|
||||
|
||||
HMSDaysUntilStart: daysUntil(hmndata.HMS2024.StartTime),
|
||||
HMSDaysUntilEnd: daysUntil(hmndata.HMS2024.EndTime),
|
||||
HMSDaysUntilStart: daysUntil(hmndata.HMS2023.StartTime),
|
||||
HMSDaysUntilEnd: daysUntil(hmndata.HMS2023.EndTime),
|
||||
|
||||
HMBostonDaysUntilStart: daysUntil(hmndata.HMBoston2024.StartTime),
|
||||
HMBostonDaysUntilEnd: daysUntil(hmndata.HMBoston2024.EndTime),
|
||||
HMBostonDaysUntilStart: daysUntil(hmndata.HMBoston2023.StartTime),
|
||||
HMBostonDaysUntilEnd: daysUntil(hmndata.HMBoston2023.EndTime),
|
||||
}, c.Perf)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render landing page template"))
|
||||
|
|
|
@ -3,7 +3,6 @@ package website
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
|
@ -455,15 +454,6 @@ func (rd *ResponseData) MustWriteTemplate(name string, data interface{}, rp *per
|
|||
}
|
||||
}
|
||||
|
||||
func (rd *ResponseData) WriteJson(data any, rp *perf.RequestPerf) {
|
||||
dataJson, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rd.Header().Set("Content-Type", "application/json")
|
||||
rd.Write(dataJson)
|
||||
}
|
||||
|
||||
func doRequest(rw http.ResponseWriter, c *RequestContext, h Handler) {
|
||||
defer func() {
|
||||
/*
|
||||
|
|
|
@ -55,10 +55,13 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
|||
hmnOnly.GET(hmnurl.RegexProjectSubmissionGuidelines, ProjectSubmissionGuidelines)
|
||||
hmnOnly.GET(hmnurl.RegexConferences, Conferences)
|
||||
hmnOnly.GET(hmnurl.RegexWhenIsIt, WhenIsIt)
|
||||
hmnOnly.GET(hmnurl.RegexNewsletterSignup, func(c *RequestContext) ResponseData {
|
||||
return c.Redirect("https://cdn.forms-content.sg-form.com/9c83182a-f04a-11ed-a42d-f6f307313b7c", http.StatusFound)
|
||||
})
|
||||
|
||||
hmnOnly.GET(hmnurl.RegexJamsIndex, JamsIndex)
|
||||
hmnOnly.GET(hmnurl.RegexJamIndex, func(c *RequestContext) ResponseData {
|
||||
return c.Redirect(hmnurl.BuildJamSaveTheDate(), http.StatusFound)
|
||||
return c.Redirect(hmnurl.BuildJamIndex2023(), http.StatusFound)
|
||||
})
|
||||
hmnOnly.GET(hmnurl.RegexJamIndex2021, JamIndex2021)
|
||||
hmnOnly.GET(hmnurl.RegexJamIndex2022, JamIndex2022)
|
||||
|
@ -68,10 +71,6 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
|||
hmnOnly.GET(hmnurl.RegexJamRecap2023_Visibility, JamRecap2023_Visibility)
|
||||
hmnOnly.GET(hmnurl.RegexJamIndex2023, JamIndex2023)
|
||||
hmnOnly.GET(hmnurl.RegexJamFeed2023, JamFeed2023)
|
||||
hmnOnly.GET(hmnurl.RegexJamIndex2024_Learning, JamIndex2024_Learning)
|
||||
hmnOnly.GET(hmnurl.RegexJamFeed2024_Learning, JamFeed2024_Learning)
|
||||
hmnOnly.GET(hmnurl.RegexJamGuidelines2024_Learning, JamGuidelines2024_Learning)
|
||||
hmnOnly.GET(hmnurl.RegexJamSaveTheDate, JamSaveTheDate)
|
||||
|
||||
hmnOnly.GET(hmnurl.RegexTimeMachine, TimeMachine)
|
||||
hmnOnly.GET(hmnurl.RegexTimeMachineSubmissions, TimeMachineSubmissions)
|
||||
|
@ -80,9 +79,6 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
|||
hmnOnly.GET(hmnurl.RegexTimeMachineFormDone, needsAuth(TimeMachineFormDone))
|
||||
hmnOnly.POST(hmnurl.RegexTimeMachineForm, needsAuth(csrfMiddleware(TimeMachineFormSubmit)))
|
||||
|
||||
hmnOnly.GET(hmnurl.RegexCalendarIndex, CalendarIndex)
|
||||
hmnOnly.GET(hmnurl.RegexCalendarICal, CalendarICal)
|
||||
|
||||
hmnOnly.GET(hmnurl.RegexStaffRolesIndex, StaffRolesIndex)
|
||||
hmnOnly.GET(hmnurl.RegexStaffRole, StaffRole)
|
||||
|
||||
|
@ -124,10 +120,9 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
|||
hmnOnly.GET(hmnurl.RegexDiscordOAuthCallback, DiscordOAuthCallback)
|
||||
hmnOnly.POST(hmnurl.RegexDiscordUnlink, needsAuth(csrfMiddleware(DiscordUnlink)))
|
||||
hmnOnly.POST(hmnurl.RegexDiscordShowcaseBacklog, needsAuth(csrfMiddleware(DiscordShowcaseBacklog)))
|
||||
hmnOnly.GET(hmnurl.RegexDiscordBotDebugPage, adminsOnly(DiscordBotDebugPage))
|
||||
|
||||
hmnOnly.POST(hmnurl.RegexTwitchEventSubCallback, TwitchEventSubCallback)
|
||||
hmnOnly.GET(hmnurl.RegexTwitchDebugPage, adminsOnly(TwitchDebugPage))
|
||||
hmnOnly.GET(hmnurl.RegexTwitchDebugPage, TwitchDebugPage)
|
||||
|
||||
hmnOnly.GET(hmnurl.RegexUserProfile, UserProfile)
|
||||
hmnOnly.GET(hmnurl.RegexUserSettings, needsAuth(UserSettings))
|
||||
|
@ -158,16 +153,11 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
|||
hmnOnly.POST(hmnurl.RegexEducationArticleDelete, educationAuthorsOnly(csrfMiddleware(EducationArticleDeleteSubmit)))
|
||||
|
||||
hmnOnly.POST(hmnurl.RegexAPICheckUsername, csrfMiddleware(APICheckUsername))
|
||||
hmnOnly.POST(hmnurl.RegexAPINewsletterSignup, APINewsletterSignup)
|
||||
|
||||
hmnOnly.GET(hmnurl.RegexLibraryAny, func(c *RequestContext) ResponseData {
|
||||
return c.Redirect(hmnurl.BuildEducationIndex(), http.StatusFound)
|
||||
})
|
||||
|
||||
hmnOnly.GET(hmnurl.RegexUnwind, func(c *RequestContext) ResponseData {
|
||||
return c.Redirect("https://www.youtube.com/playlist?list=PL-IPpPzBYXBGsAd9-c2__x6LJG4Zszs0T", http.StatusFound)
|
||||
})
|
||||
|
||||
// Project routes can appear either at the root (e.g. hero.handmade.network/edit)
|
||||
// or on a personal project path (e.g. handmade.network/p/123/hero/edit). So, we
|
||||
// have pulled all those routes into this function.
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
|
||||
"git.handmade.network/hmn/hmn/src/assets"
|
||||
"git.handmade.network/hmn/hmn/src/auth"
|
||||
"git.handmade.network/hmn/hmn/src/calendar"
|
||||
"git.handmade.network/hmn/hmn/src/config"
|
||||
"git.handmade.network/hmn/hmn/src/db"
|
||||
"git.handmade.network/hmn/hmn/src/discord"
|
||||
|
@ -53,7 +52,6 @@ var WebsiteCommand = &cobra.Command{
|
|||
twitch.MonitorTwitchSubscriptions(backgroundJobContext, conn),
|
||||
hmns3.StartServer(backgroundJobContext),
|
||||
assets.BackgroundPreviewGeneration(backgroundJobContext, conn),
|
||||
calendar.MonitorCalendars(backgroundJobContext),
|
||||
)
|
||||
|
||||
signals := make(chan os.Signal, 1)
|
||||
|
|