Do jam showcase stuff
This commit is contained in:
parent
776c78913a
commit
251446d6e4
|
@ -76,6 +76,7 @@ type DiscordConfig struct {
|
||||||
MemberRoleID string
|
MemberRoleID string
|
||||||
ShowcaseChannelID string
|
ShowcaseChannelID string
|
||||||
LibraryChannelID string
|
LibraryChannelID string
|
||||||
|
JamShowcaseChannelID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type EpisodeGuide struct {
|
type EpisodeGuide struct {
|
||||||
|
|
|
@ -582,7 +582,7 @@ func (bot *botInstance) messageCreateOrUpdate(ctx context.Context, msg *Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg.ChannelID == config.Config.Discord.ShowcaseChannelID {
|
if msg.ChannelID == config.Config.Discord.ShowcaseChannelID {
|
||||||
err := bot.processShowcaseMsg(ctx, msg)
|
err := bot.processShowcaseMsg(ctx, msg, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to process showcase message")
|
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to process showcase message")
|
||||||
return nil
|
return nil
|
||||||
|
@ -590,6 +590,15 @@ func (bot *botInstance) messageCreateOrUpdate(ctx context.Context, msg *Message)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msg.ChannelID == config.Config.Discord.JamShowcaseChannelID {
|
||||||
|
err := bot.processShowcaseMsg(ctx, msg, true)
|
||||||
|
if err != nil {
|
||||||
|
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to process jam showcase message")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if msg.ChannelID == config.Config.Discord.LibraryChannelID {
|
if msg.ChannelID == config.Config.Discord.LibraryChannelID {
|
||||||
err := bot.processLibraryMsg(ctx, msg)
|
err := bot.processLibraryMsg(ctx, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -25,7 +25,8 @@ var reDiscordMessageLink = regexp.MustCompile(`https?://.+?(\s|$)`)
|
||||||
|
|
||||||
var errNotEnoughInfo = errors.New("Discord didn't send enough info in this event for us to do this")
|
var errNotEnoughInfo = errors.New("Discord didn't send enough info in this event for us to do this")
|
||||||
|
|
||||||
func (bot *botInstance) processShowcaseMsg(ctx context.Context, msg *Message) error {
|
// TODO: Turn this ad-hoc isJam parameter into a tag or something
|
||||||
|
func (bot *botInstance) processShowcaseMsg(ctx context.Context, msg *Message, isJam bool) error {
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
case MessageTypeDefault, MessageTypeReply, MessageTypeApplicationCommand:
|
case MessageTypeDefault, MessageTypeReply, MessageTypeApplicationCommand:
|
||||||
default:
|
default:
|
||||||
|
@ -57,10 +58,17 @@ func (bot *botInstance) processShowcaseMsg(ctx context.Context, msg *Message) er
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if doSnippet, err := AllowedToCreateMessageSnippet(ctx, tx, newMsg.UserID); doSnippet && err == nil {
|
if doSnippet, err := AllowedToCreateMessageSnippet(ctx, tx, newMsg.UserID); doSnippet && err == nil {
|
||||||
_, err := CreateMessageSnippet(ctx, tx, msg.ID)
|
snippet, err := CreateMessageSnippet(ctx, tx, msg.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return oops.New(err, "failed to create snippet in gateway")
|
return oops.New(err, "failed to create snippet in gateway")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isJam {
|
||||||
|
_, err := tx.Exec(ctx, `UPDATE handmade_snippet SET is_jam = TRUE WHERE id = $1`, snippet.ID)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to mark snippet as a jam snippet")
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return oops.New(err, "failed to check snippet permissions in gateway")
|
return oops.New(err, "failed to check snippet permissions in gateway")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.handmade.network/hmn/hmn/src/migration/types"
|
||||||
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registerMigration(AddJamSnippetField{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddJamSnippetField struct{}
|
||||||
|
|
||||||
|
func (m AddJamSnippetField) Version() types.MigrationVersion {
|
||||||
|
return types.MigrationVersion(time.Date(2021, 9, 25, 2, 38, 10, 0, time.UTC))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m AddJamSnippetField) Name() string {
|
||||||
|
return "AddJamSnippetField"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m AddJamSnippetField) Description() string {
|
||||||
|
return "Add a special field for jam snippets"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m AddJamSnippetField) Up(ctx context.Context, tx pgx.Tx) error {
|
||||||
|
_, err := tx.Exec(ctx, `
|
||||||
|
ALTER TABLE handmade_snippet
|
||||||
|
ADD is_jam BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to add jam column")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m AddJamSnippetField) Down(ctx context.Context, tx pgx.Tx) error {
|
||||||
|
_, err := tx.Exec(ctx, `
|
||||||
|
ALTER TABLE handmade_snippet
|
||||||
|
DROP is_jam;
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to drop jam column")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -22,12 +22,19 @@
|
||||||
<title>Handmade Network</title>
|
<title>Handmade Network</title>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
<script src="{{ static "js/templates.js" }}"></script>
|
||||||
|
<script src="{{ static "js/showcase.js" }}"></script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{ static "fonts/mohave/stylesheet.css" }}">
|
<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+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/css?family=Fira+Mono:300,400,500,700' rel='stylesheet' type='text/css'>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static "style.css" }}">
|
<link rel="stylesheet" type="text/css" href="{{ static "style.css" }}">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
:root {
|
||||||
|
--content-background: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: linear-gradient(#ab4c47, #a5467d);
|
background: linear-gradient(#ab4c47, #a5467d);
|
||||||
}
|
}
|
||||||
|
@ -171,6 +178,11 @@
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.showcase-item {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 30em) {
|
@media screen and (min-width: 30em) {
|
||||||
/* not small styles */
|
/* not small styles */
|
||||||
|
|
||||||
|
@ -256,6 +268,16 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="showcase-outer-container" class="bg-black-20 pt4 pb3 pb4-ns">
|
||||||
|
<div class="section mw8 margin-center ph3 ph4-l">
|
||||||
|
<h2>Happening right now.</h2>
|
||||||
|
<p>
|
||||||
|
The jam is underway! These screenshots and videos were shared in #jam-showcase on our <a href="https://discord.gg/hxWxDee" target="_blank">Discord</a>. Join us!
|
||||||
|
</p>
|
||||||
|
<div id="showcase-container" class="mw8 center-layout mh2 mh0-ns"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section flex-fair mw8 margin-center ph3 ph4-l mv4">
|
<div class="section flex-fair mw8 margin-center ph3 ph4-l mv4">
|
||||||
<h2>Why reinvent the wheel?</h2>
|
<h2>Why reinvent the wheel?</h2>
|
||||||
<p>
|
<p>
|
||||||
|
@ -346,6 +368,106 @@
|
||||||
{{ template "footer.html" . }}
|
{{ template "footer.html" . }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{ template "showcase_templates.html" }}
|
||||||
|
|
||||||
|
<!-- Copy-pasted and mangled from showcase.html -->
|
||||||
|
<script>
|
||||||
|
const ROW_HEIGHT = 300;
|
||||||
|
const ITEM_SPACING = 4;
|
||||||
|
|
||||||
|
const showcaseItems = JSON.parse("{{ .ShowcaseItemsJSON }}");
|
||||||
|
const addThumbnailFuncs = new Array(showcaseItems.length);
|
||||||
|
|
||||||
|
const showcaseOuterContainer = document.querySelector('#showcase-outer-container');
|
||||||
|
let showcaseContainer = document.querySelector('#showcase-container');
|
||||||
|
|
||||||
|
showcaseOuterContainer.classList.toggle('dn', showcaseItems.length === 0);
|
||||||
|
|
||||||
|
const itemElements = []; // array of arrays
|
||||||
|
for (let i = 0; i < showcaseItems.length; i++) {
|
||||||
|
const item = showcaseItems[i];
|
||||||
|
|
||||||
|
const [itemEl, addThumbnail] = makeShowcaseItem(item);
|
||||||
|
itemEl.container.setAttribute('data-index', i);
|
||||||
|
itemEl.container.setAttribute('data-date', item.date);
|
||||||
|
|
||||||
|
addThumbnailFuncs[i] = addThumbnail;
|
||||||
|
|
||||||
|
itemElements.push(itemEl.container);
|
||||||
|
}
|
||||||
|
|
||||||
|
function layout() {
|
||||||
|
const width = showcaseContainer.getBoundingClientRect().width;
|
||||||
|
showcaseContainer = emptyElement(showcaseContainer);
|
||||||
|
|
||||||
|
function addRow(itemEls, rowWidth, container) {
|
||||||
|
const totalSpacing = ITEM_SPACING * (itemEls.length - 1);
|
||||||
|
const scaleFactor = (width / Math.max(rowWidth, width));
|
||||||
|
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.classList.add('flex');
|
||||||
|
row.classList.toggle('justify-between', rowWidth >= width);
|
||||||
|
row.style.marginBottom = `${ITEM_SPACING}px`;
|
||||||
|
|
||||||
|
for (const itemEl of itemEls) {
|
||||||
|
const index = parseInt(itemEl.getAttribute('data-index'), 10);
|
||||||
|
const item = showcaseItems[index];
|
||||||
|
|
||||||
|
const aspect = item.width / item.height;
|
||||||
|
const baseWidth = (aspect * ROW_HEIGHT) * scaleFactor;
|
||||||
|
const actualWidth = baseWidth - (totalSpacing / itemEls.length);
|
||||||
|
|
||||||
|
itemEl.style.width = `${actualWidth}px`;
|
||||||
|
itemEl.style.height = `${scaleFactor * ROW_HEIGHT}px`;
|
||||||
|
itemEl.style.marginRight = `${ITEM_SPACING}px`;
|
||||||
|
|
||||||
|
row.appendChild(itemEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rowItemEls = [];
|
||||||
|
let rowWidth = 0;
|
||||||
|
|
||||||
|
for (const itemEl of itemElements) {
|
||||||
|
const index = parseInt(itemEl.getAttribute('data-index'), 10);
|
||||||
|
const item = showcaseItems[index];
|
||||||
|
|
||||||
|
const aspect = item.width / item.height;
|
||||||
|
rowWidth += aspect * ROW_HEIGHT;
|
||||||
|
|
||||||
|
rowItemEls.push(itemEl);
|
||||||
|
|
||||||
|
if (rowWidth > width) {
|
||||||
|
addRow(rowItemEls, rowWidth, showcaseContainer);
|
||||||
|
|
||||||
|
rowItemEls = [];
|
||||||
|
rowWidth = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addRow(rowItemEls, rowWidth, showcaseContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadImages() {
|
||||||
|
const items = showcaseContainer.querySelectorAll('.showcase-item');
|
||||||
|
for (const item of items) {
|
||||||
|
const i = parseInt(item.getAttribute('data-index'), 10);
|
||||||
|
addThumbnailFuncs[i]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout();
|
||||||
|
layout(); // scrollbars are fun!!
|
||||||
|
|
||||||
|
loadImages();
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
layout();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -238,6 +238,8 @@ func AtomFeed(c *RequestContext) ResponseData {
|
||||||
INNER JOIN auth_user AS owner ON owner.id = snippet.owner_id
|
INNER JOIN auth_user AS owner ON owner.id = snippet.owner_id
|
||||||
LEFT JOIN handmade_asset AS asset ON asset.id = snippet.asset_id
|
LEFT JOIN handmade_asset AS asset ON asset.id = snippet.asset_id
|
||||||
LEFT JOIN handmade_discordmessage AS discord_message ON discord_message.id = snippet.discord_message_id
|
LEFT JOIN handmade_discordmessage AS discord_message ON discord_message.id = snippet.discord_message_id
|
||||||
|
WHERE
|
||||||
|
NOT snippet.is_jam
|
||||||
ORDER BY snippet.when DESC
|
ORDER BY snippet.when DESC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
`,
|
`,
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
package website
|
package website
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
"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/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,6 +20,45 @@ func JamIndex(c *RequestContext) ResponseData {
|
||||||
daysUntil = 0
|
daysUntil = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SQL", "Fetch showcase snippets")
|
||||||
|
type snippetQuery struct {
|
||||||
|
Owner models.User `db:"owner"`
|
||||||
|
Snippet models.Snippet `db:"snippet"`
|
||||||
|
Asset *models.Asset `db:"asset"`
|
||||||
|
DiscordMessage *models.DiscordMessage `db:"discord_message"`
|
||||||
|
}
|
||||||
|
snippetQueryResult, err := db.Query(c.Context(), c.Conn, snippetQuery{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
handmade_snippet AS snippet
|
||||||
|
INNER JOIN auth_user AS owner ON owner.id = snippet.owner_id
|
||||||
|
LEFT JOIN handmade_asset AS asset ON asset.id = snippet.asset_id
|
||||||
|
LEFT JOIN handmade_discordmessage AS discord_message ON discord_message.id = snippet.discord_message_id
|
||||||
|
WHERE
|
||||||
|
snippet.is_jam
|
||||||
|
ORDER BY snippet.when DESC
|
||||||
|
LIMIT 20
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch jam snippets"))
|
||||||
|
}
|
||||||
|
snippetQuerySlice := snippetQueryResult.ToSlice()
|
||||||
|
showcaseItems := make([]templates.TimelineItem, 0, len(snippetQuerySlice))
|
||||||
|
for _, s := range snippetQuerySlice {
|
||||||
|
row := s.(*snippetQuery)
|
||||||
|
timelineItem := SnippetToTimelineItem(&row.Snippet, row.Asset, row.DiscordMessage, &row.Owner, c.Theme)
|
||||||
|
if timelineItem.Type != templates.TimelineTypeSnippetYoutube {
|
||||||
|
showcaseItems = append(showcaseItems, timelineItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SHOWCASE", "Convert to json")
|
||||||
|
showcaseJson := templates.TimelineItemsToJSON(showcaseItems)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
baseData := getBaseDataAutocrumb(c, "Wheel Reinvention Jam")
|
baseData := getBaseDataAutocrumb(c, "Wheel Reinvention Jam")
|
||||||
baseData.OpenGraphItems = []templates.OpenGraphItem{
|
baseData.OpenGraphItems = []templates.OpenGraphItem{
|
||||||
{Property: "og:site_name", Value: "Handmade.Network"},
|
{Property: "og:site_name", Value: "Handmade.Network"},
|
||||||
|
@ -28,11 +71,13 @@ func JamIndex(c *RequestContext) ResponseData {
|
||||||
type JamPageData struct {
|
type JamPageData struct {
|
||||||
templates.BaseData
|
templates.BaseData
|
||||||
DaysUntil int
|
DaysUntil int
|
||||||
|
ShowcaseItemsJSON string
|
||||||
}
|
}
|
||||||
|
|
||||||
res.MustWriteTemplate("wheeljam_index.html", JamPageData{
|
res.MustWriteTemplate("wheeljam_index.html", JamPageData{
|
||||||
BaseData: baseData,
|
BaseData: baseData,
|
||||||
DaysUntil: daysUntil,
|
DaysUntil: daysUntil,
|
||||||
|
ShowcaseItemsJSON: showcaseJson,
|
||||||
}, c.Perf)
|
}, c.Perf)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
@ -232,6 +232,8 @@ func Index(c *RequestContext) ResponseData {
|
||||||
INNER JOIN auth_user AS owner ON owner.id = snippet.owner_id
|
INNER JOIN auth_user AS owner ON owner.id = snippet.owner_id
|
||||||
LEFT JOIN handmade_asset AS asset ON asset.id = snippet.asset_id
|
LEFT JOIN handmade_asset AS asset ON asset.id = snippet.asset_id
|
||||||
LEFT JOIN handmade_discordmessage AS discord_message ON discord_message.id = snippet.discord_message_id
|
LEFT JOIN handmade_discordmessage AS discord_message ON discord_message.id = snippet.discord_message_id
|
||||||
|
WHERE
|
||||||
|
NOT snippet.is_jam
|
||||||
ORDER BY snippet.when DESC
|
ORDER BY snippet.when DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
`,
|
`,
|
||||||
|
|
|
@ -32,6 +32,8 @@ func Showcase(c *RequestContext) ResponseData {
|
||||||
INNER JOIN auth_user AS owner ON owner.id = snippet.owner_id
|
INNER JOIN auth_user AS owner ON owner.id = snippet.owner_id
|
||||||
LEFT JOIN handmade_asset AS asset ON asset.id = snippet.asset_id
|
LEFT JOIN handmade_asset AS asset ON asset.id = snippet.asset_id
|
||||||
LEFT JOIN handmade_discordmessage AS discord_message ON discord_message.id = snippet.discord_message_id
|
LEFT JOIN handmade_discordmessage AS discord_message ON discord_message.id = snippet.discord_message_id
|
||||||
|
WHERE
|
||||||
|
NOT snippet.is_jam
|
||||||
ORDER BY snippet.when DESC
|
ORDER BY snippet.when DESC
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue