Reworked project edit page (no preview yet)

This commit is contained in:
Ben Visness 2024-05-24 21:09:28 -05:00 committed by Ben Visness
parent be7440acce
commit d0c7605029
58 changed files with 1497 additions and 480 deletions

View File

@ -1,12 +1,15 @@
function ImageSelector(form, maxFileSize, container, defaultImageUrl) {
this.form = form;
this.maxFileSize = maxFileSize;
this.fileInput = container.querySelector(".image_input");
this.removeImageInput = container.querySelector(".remove_input");
this.fileInput = container.querySelector(".imginput");
this.removeImageInput = container.querySelector(".imginput-remove");
this.imageEl = container.querySelector("img");
this.resetLink = container.querySelector(".reset");
this.removeLink = container.querySelector(".remove");
this.originalImageUrl = this.imageEl.getAttribute("data-original");
this.container = container.querySelector(".imginput-container");
this.resetLink = container.querySelector(".imginput-reset-link");
this.removeLink = container.querySelector(".imginput-remove-link");
this.filenameText = container.querySelector(".imginput-filename");
this.originalImageUrl = this.imageEl.getAttribute("data-imginput-original");
this.originalImageFilename = this.imageEl.getAttribute("data-imginput-original-filename");
this.currentImageUrl = this.originalImageUrl;
this.defaultImageUrl = defaultImageUrl || "";
@ -14,7 +17,7 @@ function ImageSelector(form, maxFileSize, container, defaultImageUrl) {
this.removeImageInput.value = "";
this.setImageUrl(this.originalImageUrl);
this.updateButtons();
this.updatePreview();
this.fileInput.addEventListener("change", function(ev) {
if (this.fileInput.files.length > 0) {
@ -33,12 +36,16 @@ function ImageSelector(form, maxFileSize, container, defaultImageUrl) {
}
}
ImageSelector.prototype.openFileInput = function() {
this.fileInput.click();
}
ImageSelector.prototype.handleNewImageFile = function(file) {
if (file) {
this.updateSizeLimit(file.size);
this.removeImageInput.value = "";
this.setImageUrl(URL.createObjectURL(file));
this.updateButtons();
this.updatePreview(file);
}
};
@ -47,7 +54,7 @@ ImageSelector.prototype.removeImage = function() {
this.fileInput.value = "";
this.removeImageInput.value = "true";
this.setImageUrl(this.defaultImageUrl);
this.updateButtons();
this.updatePreview(null);
};
ImageSelector.prototype.resetImage = function() {
@ -55,7 +62,7 @@ ImageSelector.prototype.resetImage = function() {
this.fileInput.value = "";
this.removeImageInput.value = "";
this.setImageUrl(this.originalImageUrl);
this.updateButtons();
this.updatePreview(null);
};
ImageSelector.prototype.updateSizeLimit = function(size) {
@ -82,19 +89,24 @@ ImageSelector.prototype.setImageUrl = function(url) {
}
};
ImageSelector.prototype.updateButtons = function() {
if ((this.originalImageUrl.length > 0 && this.originalImageUrl != this.defaultImageUrl)
&& this.currentImageUrl != this.originalImageUrl) {
ImageSelector.prototype.updatePreview = function(file) {
const showReset = (
this.originalImageUrl
&& this.originalImageUrl != this.defaultImageUrl
&& this.originalImageUrl != this.currentImageUrl
);
const showRemove = (
!this.fileInput.required
&& this.currentImageUrl != this.defaultImageUrl
);
this.resetLink.hidden = !showReset;
this.removeLink.hidden = !showRemove;
this.resetLink.style.display = "inline-block";
if (this.currentImageUrl == this.originalImageUrl) {
this.filenameText.innerText = this.originalImageFilename;
} else {
this.resetLink.style.display = "none";
this.filenameText.innerText = file ? file.name : "";
}
if (!this.fileInput.required && this.currentImageUrl != this.defaultImageUrl) {
this.removeLink.style.display = "inline-block";
} else {
this.removeLink.style.display = "none";
}
this.container.hidden = !this.currentImageUrl;
};

3
public/js/script.js Normal file
View File

@ -0,0 +1,3 @@
function rem2px(rem) {
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
}

View File

@ -139,7 +139,7 @@ summary {
}
[hidden],
template {
display: none;
display: none !important;
}
.border-box,
a,
@ -7157,7 +7157,7 @@ code {
:root {
--background-color: white;
--color: black;
--link-color: #cc3b95;
--link-color: #d12991;
--red: #c61d24;
--dim-color: #333;
--dimmer-color: #999;
@ -7170,10 +7170,9 @@ code {
--theme-color-light: #666;
--main-background-color: #f8f8f8;
--main-background-color-transparent: rgba(#f8f8f8, 0);
--card-background: #e8e8e8;
--card-background-hover: #f0f0f0;
--dim-background: #f0f0f0;
--dim-background-transparent: rgba(#f0f0f0, 0);
--bg-1: #1f1f1f;
--bg-2: #2f2f2f;
--bg-3: #494949;
--forum-thread-read-color: #555;
--forum-thread-read-link-color: #888;
--notice-hiatus-color: #aa7d30;
@ -7190,7 +7189,8 @@ code {
:root {
--background-color: #2f2f2f;
--color: #eee;
--link-color: #cc3b95;
--link-color: #ff5dc2;
--color-error: #ff6666;
--dim-color: #bbb;
--dimmer-color: #999;
--dimmest-color: #777;
@ -7205,8 +7205,15 @@ code {
--card-background: #494949;
--card-background-hover: #333;
--card-background-transparent: #242424D8;
--dim-background: #252525;
--dim-background-transparent: rgba(#252525, 0);
--bg-1: #1f1f1f;
--bg-2: #2f2f2f;
--bg-3: #494949;
--bg-4: #595959;
--bg-5: #cbcbcb;
--border-color: #595959;
--border-color-focused: #4e55ff;
--border-color-error: #ff3a3a;
--button-color-primary: #c900ea;
--forum-thread-read-color: #777;
--forum-thread-read-link-color: #999;
--notice-hiatus-color: #876327;
@ -7222,6 +7229,7 @@ code {
/* src/rawdata/scss/core.css */
* {
box-sizing: border-box;
border-color: var(--border-color);
}
br {
border-style: none;
@ -7250,7 +7258,7 @@ a.external::after,
}
b,
strong {
font-weight: 500;
font-weight: 600;
}
h1,
h2,
@ -7261,7 +7269,7 @@ h6 {
font-size: inherit;
margin: 0;
line-height: 1;
font-weight: 500;
font-weight: 600;
}
code,
pre,
@ -7271,6 +7279,12 @@ pre,
.bg--main {
background-color: var(--main-background-color);
}
.bg--card {
background-color: var(--card-background);
}
.bg--card-transparent {
background-color: var(--card-background-transparent);
}
.m--center {
margin-left: auto;
margin-right: auto;
@ -7321,14 +7335,20 @@ pre,
.b--theme-light {
border-color: var(--theme-color-light);
}
.bg--dim {
background-color: var(--dim-background);
.bg1 {
background-color: var(--bg-1);
}
.bg--content {
background-color: var(--content-background);
.bg2 {
background-color: var(--bg-2);
}
.bg--card {
background-color: var(--card-background);
.bg3 {
background-color: var(--bg-3);
}
.bg4 {
background-color: var(--bg-4);
}
.bg5 {
background-color: var(--bg-5);
}
.bg-theme {
background-color: var(--theme-color);
@ -7511,6 +7531,18 @@ pre,
.rot-180 {
transform: rotate(180deg);
}
:not([hidden]) + .show-when-sibling-hidden {
display: none;
}
.grab:hover {
cursor: grab;
}
.grabbing .grab:hover {
cursor: grabbing;
}
.grabbing {
cursor: grabbing;
}
@media screen and (min-width: 35em) {
.bi-avoid-ns {
break-inside: avoid;
@ -7551,22 +7583,22 @@ pre,
.grid-2-ns {
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; } }
.bg1-ns {
background-color: var(--bg-1);
}
.bg2-ns {
background-color: var(--bg-2);
}
.bg3-ns {
background-color: var(--bg-3);
}
.bg4-ns {
background-color: var(--bg-4);
}
.bg5-ns {
background-color: var(--bg-5);
}
}
@media screen and (min-width: 35em) and (max-width: 60em) {
.bi-avoid-m {
break-inside: avoid;
@ -7604,22 +7636,22 @@ pre,
.grid-2-m {
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; } }
.bg1-m {
background-color: var(--bg-1);
}
.bg2-m {
background-color: var(--bg-2);
}
.bg3-m {
background-color: var(--bg-3);
}
.bg4-m {
background-color: var(--bg-4);
}
.bg5-m {
background-color: var(--bg-5);
}
}
@media screen and (min-width: 60em) {
.bi-avoid-l {
break-inside: avoid;
@ -7657,24 +7689,21 @@ pre,
.grid-2-l {
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; } }
.not-first:first-child {
display: none;
.bg1-l {
background-color: var(--bg-1);
}
.bg2-l {
background-color: var(--bg-2);
}
.bg3-l {
background-color: var(--bg-3);
}
.bg4-l {
background-color: var(--bg-4);
}
.bg5-l {
background-color: var(--bg-5);
}
}
.not-first-of-type:first-of-type {
@ -7694,6 +7723,7 @@ pre,
stroke: currentColor;
width: 1em;
height: 1em;
overflow: visible;
}
.svgicon:not(.svgicon-nofix) svg {
transform: translate(0px, 0.1em);
@ -7825,6 +7855,9 @@ pre,
}
/* src/rawdata/scss/content.css */
.post-content {
line-height: 1.4;
}
.post-content *:first-child {
margin-top: 0;
}
@ -7851,11 +7884,16 @@ pre,
.post-content h3,
.post-content h4,
.post-content h5 {
line-height: 1.2;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.post-content li:not(:last-child) {
margin-bottom: 0.2em;
margin-bottom: 0.6em;
}
.post-content li p {
margin-top: 0.6em;
margin-bottom: 0.6em;
}
.post-content img {
max-width: 100%;
@ -7892,7 +7930,6 @@ pre,
}
.post-content code {
background-color: var(--dim-background);
border-radius: var(--border-radius-2);
padding: .2em 0;
white-space: nowrap;
}
@ -7905,7 +7942,6 @@ pre,
.post-content pre > code,
.post-content pre.hmn-code {
background-color: var(--dim-background);
border-radius: var(--border-radius-2);
padding: 0.7em;
overflow-x: auto;
}
@ -8067,6 +8103,101 @@ pre,
color: red;
}
/* src/rawdata/scss/form.css */
.hmn-form input,
.hmn-form textarea,
.hmn-form select {
color: var(--color);
accent-color: var(--button-color-primary);
background-color: var(--bg-3);
padding: 0.75rem;
outline: none;
}
.hmn-form input:not(.no-border),
.hmn-form textarea:not(.no-border),
.hmn-form select:not(.no-border) {
border: 1px solid var(--border-color);
}
.hmn-form input:focus,
.hmn-form textarea:focus,
.hmn-form select:focus {
border-color: var(--border-color-focused);
}
.hmn-form input:focus ~ .also-focus,
.hmn-form textarea:focus ~ .also-focus,
.hmn-form select:focus ~ .also-focus {
border-color: var(--border-color-focused);
}
.error :is(.hmn-form input),
.hmn-form input.error,
.hmn-form input:invalid,
.error :is(.hmn-form textarea),
.hmn-form textarea.error,
.hmn-form textarea:invalid,
.error :is(.hmn-form select),
.hmn-form select.error,
.hmn-form select:invalid {
border-color: var(--border-color-error);
}
.hmn-form input:disabled,
.hmn-form textarea:disabled,
.hmn-form select:disabled {
background-color: var(--bg-5);
color: var(--border-color);
}
.hmn-form textarea {
resize: vertical;
}
.hmn-form button,
.hmn-form input[type=submit] {
color: var(--color);
background-color: var(--bg-3);
cursor: pointer;
font-weight: 500;
line-height: 1.5rem;
}
.hmn-form button.btn-primary,
.hmn-form input[type=submit].btn-primary {
background-color: var(--button-color-primary);
}
.hmn-form button:not(.no-border),
.hmn-form input[type=submit]:not(.no-border) {
border: none;
}
.hmn-form button:not(.no-padding),
.hmn-form input[type=submit]:not(.no-padding) {
padding: 0.5rem 1.5rem;
}
.hmn-form label {
font-weight: 600;
}
.hmn-form .input-group {
display: flex;
flex-direction: column;
gap: var(--spacing-extra-small);
line-height: 1.4;
}
.hmn-form .error .error-msg {
color: var(--color-error);
}
.hmn-form fieldset {
margin: 0;
padding: 0;
border: 1px solid var(--border-color);
background-color: var(--bg-2);
display: flex;
flex-direction: column;
}
.hmn-form legend {
background-color: var(--bg-1);
font-weight: bold;
float: left;
padding: var(--spacing-medium);
}
.hmn-form legend:not(:last-child) {
border-bottom: 1px solid var(--border-color);
}
/* src/rawdata/scss/forum.css */
.thread-list-item .latestpost {
width: 16.5rem;
@ -8299,7 +8430,7 @@ header.old .submenu > a {
}
}
header {
background-color: var(--card-background);
background-color: var(--bg-4);
}
header .hmn-logo {
font-family: "MohaveHMN", sans-serif;
@ -8523,6 +8654,193 @@ span.icon-rss::before {
background-image: linear-gradient(rgba(0, 0, 0, 0.82), rgba(0, 0, 0, 0));
}
/* src/rawdata/scss/syntax.css */
pre .hll,
code .hll,
.codeblock .hll {
background-color: #ffffcc;
}
pre .c,
code .c,
.codeblock .c {
color: #008000;
}
pre .err,
code .err,
.codeblock .err {
border: 1px solid #FF0000;
}
pre .k,
code .k,
.codeblock .k {
color: #0000ff;
}
pre .ch,
code .ch,
.codeblock .ch {
color: #008000;
}
pre .cm,
code .cm,
.codeblock .cm {
color: #008000;
}
pre .cp,
code .cp,
.codeblock .cp {
color: #0000ff;
}
pre .cpf,
code .cpf,
.codeblock .cpf {
color: #008000;
}
pre .c1,
code .c1,
.codeblock .c1 {
color: #008000;
}
pre .cs,
code .cs,
.codeblock .cs {
color: #008000;
}
pre .ge,
code .ge,
.codeblock .ge {
font-style: italic;
}
pre .gh,
code .gh,
.codeblock .gh {
font-weight: bold;
}
pre .gp,
code .gp,
.codeblock .gp {
font-weight: bold;
}
pre .gs,
code .gs,
.codeblock .gs {
font-weight: bold;
}
pre .gu,
code .gu,
.codeblock .gu {
font-weight: bold;
}
pre .kc,
code .kc,
.codeblock .kc {
color: #0000ff;
}
pre .kd,
code .kd,
.codeblock .kd {
color: #0000ff;
}
pre .kn,
code .kn,
.codeblock .kn {
color: #0000ff;
}
pre .kp,
code .kp,
.codeblock .kp {
color: #0000ff;
}
pre .kr,
code .kr,
.codeblock .kr {
color: #0000ff;
}
pre .kt,
code .kt,
.codeblock .kt {
color: #2b91af;
}
pre .s,
code .s,
.codeblock .s {
color: #a31515;
}
pre .nc,
code .nc,
.codeblock .nc {
color: #2b91af;
}
pre .ow,
code .ow,
.codeblock .ow {
color: #0000ff;
}
pre .sa,
code .sa,
.codeblock .sa {
color: #a31515;
}
pre .sb,
code .sb,
.codeblock .sb {
color: #a31515;
}
pre .sc,
code .sc,
.codeblock .sc {
color: #a31515;
}
pre .dl,
code .dl,
.codeblock .dl {
color: #a31515;
}
pre .sd,
code .sd,
.codeblock .sd {
color: #a31515;
}
pre .s2,
code .s2,
.codeblock .s2 {
color: #a31515;
}
pre .se,
code .se,
.codeblock .se {
color: #a31515;
}
pre .sh,
code .sh,
.codeblock .sh {
color: #a31515;
}
pre .si,
code .si,
.codeblock .si {
color: #a31515;
}
pre .sx,
code .sx,
.codeblock .sx {
color: #a31515;
}
pre .sr,
code .sr,
.codeblock .sr {
color: #a31515;
}
pre .s1,
code .s1,
.codeblock .s1 {
color: #a31515;
}
pre .ss,
code .ss,
.codeblock .ss {
color: #a31515;
}
/* src/rawdata/scss/timeline.css */
.avatar {
object-fit: cover;

View File

@ -11223,7 +11223,7 @@ blockquote,
border-color: var(--theme-color-dimmest);
}
.bg--dim,
.bg3,
.post-content code,
.post-content pre>code,
.post-content pre.hmn-code,
@ -11463,7 +11463,7 @@ figure {
grid-template-columns: 1fr 1fr;
}
.bg--dim-ns {
.bg3-ns {
background-color: #f0f0f0;
background-color: var(--dim-background);
}
@ -11518,7 +11518,7 @@ figure {
grid-template-columns: 1fr 1fr;
}
.bg--dim-m {
.bg3-m {
background-color: #f0f0f0;
background-color: var(--dim-background);
}
@ -11573,7 +11573,7 @@ figure {
grid-template-columns: 1fr 1fr;
}
.bg--dim-l {
.bg3-l {
background-color: #f0f0f0;
background-color: var(--dim-background);
}

View File

@ -38,8 +38,9 @@ type ProjectsQuery struct {
type ProjectAndStuff struct {
Project models.Project
LogoLightAsset *models.Asset `db:"logolight_asset"`
LogoDarkAsset *models.Asset `db:"logodark_asset"`
LogoLightAsset *models.Asset
LogoDarkAsset *models.Asset
HeaderImage *models.Asset
Owners []*models.User
Tag *models.Tag
}
@ -72,6 +73,7 @@ func FetchProjects(
Project models.Project `db:"project"`
LogoLightAsset *models.Asset `db:"logolight_asset"`
LogoDarkAsset *models.Asset `db:"logodark_asset"`
HeaderAsset *models.Asset `db:"header_asset"`
Tag *models.Tag `db:"tag"`
}
@ -86,6 +88,7 @@ func FetchProjects(
project
LEFT JOIN asset AS logolight_asset ON logolight_asset.id = project.logolight_asset_id
LEFT JOIN asset AS logodark_asset ON logodark_asset.id = project.logodark_asset_id
LEFT JOIN asset AS header_asset ON header_asset.id = project.header_asset_id
LEFT JOIN tag ON project.tag = tag.id
`)
if len(q.OwnerIDs) > 0 {
@ -219,6 +222,7 @@ func FetchProjects(
Project: p.Project,
LogoLightAsset: p.LogoLightAsset,
LogoDarkAsset: p.LogoDarkAsset,
HeaderImage: p.HeaderAsset,
Owners: owners,
Tag: p.Tag,
})

View File

@ -0,0 +1,47 @@
package migrations
import (
"context"
"time"
"git.handmade.network/hmn/hmn/src/migration/types"
"github.com/jackc/pgx/v5"
)
func init() {
registerMigration(AddHeaderImage{})
}
type AddHeaderImage struct{}
func (m AddHeaderImage) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2024, 6, 1, 2, 1, 18, 0, time.UTC))
}
func (m AddHeaderImage) Name() string {
return "AddHeaderImage"
}
func (m AddHeaderImage) Description() string {
return "Adds a header image to projects"
}
func (m AddHeaderImage) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
ALTER TABLE project
ADD COLUMN header_asset_id UUID REFERENCES asset (id) ON DELETE SET NULL;
`,
)
return err
}
func (m AddHeaderImage) Down(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
ALTER TABLE project
DROP COLUMN header_asset_id;
`,
)
return err
}

View File

@ -1,4 +1,6 @@
.post-content {
line-height: 1.4;
* {
&:first-child {
margin-top: 0;
@ -34,12 +36,18 @@
h3,
h4,
h5 {
line-height: 1.2;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
li:not(:last-child) {
margin-bottom: 0.2em;
margin-bottom: 0.6em;
}
li p {
margin-top: 0.6em;
margin-bottom: 0.6em;
}
img {
@ -84,7 +92,6 @@
code {
background-color: var(--dim-background);
border-radius: var(--border-radius-2);
padding: .2em 0;
white-space: nowrap;
@ -100,7 +107,6 @@
pre>code,
pre.hmn-code {
background-color: var(--dim-background);
border-radius: var(--border-radius-2);
padding: 0.7em;
overflow-x: auto;

View File

@ -3,6 +3,7 @@
* {
/* It's aggressive, but we like it aggressive */
box-sizing: border-box;
border-color: var(--border-color);
}
br {
@ -37,7 +38,7 @@ a,
b,
strong {
font-weight: 500;
font-weight: 600;
}
h1,
@ -49,7 +50,7 @@ h6 {
font-size: inherit;
margin: 0;
line-height: 1;
font-weight: 500;
font-weight: 600;
}
code,
@ -64,6 +65,14 @@ pre,
background-color: var(--main-background-color);
}
.bg--card {
background-color: var(--card-background);
}
.bg--card-transparent {
background-color: var(--card-background-transparent);
}
.m--center {
margin-left: auto;
margin-right: auto;
@ -135,16 +144,24 @@ pre,
border-color: var(--theme-color-light);
}
.bg--dim {
background-color: var(--dim-background);
.bg1 {
background-color: var(--bg-1);
}
.bg--content {
background-color: var(--content-background);
.bg2 {
background-color: var(--bg-2);
}
.bg--card {
background-color: var(--card-background);
.bg3 {
background-color: var(--bg-3);
}
.bg4 {
background-color: var(--bg-4);
}
.bg5 {
background-color: var(--bg-5);
}
.bg-theme {
@ -388,6 +405,22 @@ pre,
transform: rotate(180deg);
}
:not([hidden])+.show-when-sibling-hidden {
display: none;
}
.grab:hover {
cursor: grab;
.grabbing & {
cursor: grabbing;
}
}
.grabbing {
cursor: grabbing;
}
@media screen and (min-width: 35em) {
.bi-avoid-ns {
break-inside: avoid;
@ -441,8 +474,24 @@ pre,
grid-template-columns: 1fr 1fr;
}
.bg--dim-ns {
background-color: var(--dim-background);
.bg1-ns {
background-color: var(--bg-1);
}
.bg2-ns {
background-color: var(--bg-2);
}
.bg3-ns {
background-color: var(--bg-3);
}
.bg4-ns {
background-color: var(--bg-4);
}
.bg5-ns {
background-color: var(--bg-5);
}
}
@ -495,8 +544,24 @@ pre,
grid-template-columns: 1fr 1fr;
}
.bg--dim-m {
background-color: var(--dim-background);
.bg1-m {
background-color: var(--bg-1);
}
.bg2-m {
background-color: var(--bg-2);
}
.bg3-m {
background-color: var(--bg-3);
}
.bg4-m {
background-color: var(--bg-4);
}
.bg5-m {
background-color: var(--bg-5);
}
}
@ -549,8 +614,24 @@ pre,
grid-template-columns: 1fr 1fr;
}
.bg--dim-l {
background-color: var(--dim-background);
.bg1-l {
background-color: var(--bg-1);
}
.bg2-l {
background-color: var(--bg-2);
}
.bg3-l {
background-color: var(--bg-3);
}
.bg4-l {
background-color: var(--bg-4);
}
.bg5-l {
background-color: var(--bg-5);
}
}
@ -560,6 +641,7 @@ pre,
stroke: currentColor;
width: 1em;
height: 1em;
overflow: visible;
}
&:not(.svgicon-nofix) svg {

95
src/rawdata/scss/form.css Normal file
View File

@ -0,0 +1,95 @@
.hmn-form {
input,
textarea,
select {
color: var(--color);
accent-color: var(--button-color-primary);
background-color: var(--bg-3);
padding: 0.75rem;
outline: none;
&:not(.no-border) {
border: 1px solid var(--border-color);
}
&:focus {
border-color: var(--border-color-focused);
~.also-focus {
border-color: var(--border-color-focused);
}
}
.error &,
&.error,
&:invalid {
border-color: var(--border-color-error);
}
&:disabled {
background-color: var(--bg-5);
color: var(--border-color);
}
}
textarea {
resize: vertical;
}
button,
input[type=submit] {
color: var(--color);
background-color: var(--bg-3);
cursor: pointer;
font-weight: 500;
line-height: 1.5rem;
&.btn-primary {
background-color: var(--button-color-primary);
}
&:not(.no-border) {
border: none;
}
&:not(.no-padding) {
padding: 0.5rem 1.5rem;
}
}
label {
font-weight: 600;
}
.input-group {
display: flex;
flex-direction: column;
gap: var(--spacing-extra-small);
line-height: 1.4;
}
.error .error-msg {
color: var(--color-error);
}
fieldset {
margin: 0;
padding: 0;
border: 1px solid var(--border-color);
background-color: var(--bg-2);
display: flex;
flex-direction: column;
}
legend {
background-color: var(--bg-1);
font-weight: bold;
float: left;
padding: var(--spacing-medium);
&:not(:last-child) {
border-bottom: 1px solid var(--border-color);
}
}
}

View File

@ -114,7 +114,7 @@ header.old {
}
header {
background-color: var(--card-background);
background-color: var(--bg-4);
.hmn-logo {
font-family: 'MohaveHMN', sans-serif;

View File

@ -7,6 +7,7 @@
@import "content.css";
@import "editor.css";
@import "education.css";
@import "form.css";
@import "forum.css";
@import "header.css";
@import "icons.css";
@ -14,4 +15,5 @@
@import "progress_bar.css";
@import "projects.css";
@import "showcase.css";
@import "syntax.css";
@import "timeline.css";

View File

@ -0,0 +1,39 @@
pre, code, .codeblock {
.hll { background-color: #ffffcc }
.c { color: #008000 } /* Comment */
.err { border: 1px solid #FF0000 } /* Error */
.k { color: #0000ff } /* Keyword */
.ch { color: #008000 } /* Comment.Hashbang */
.cm { color: #008000 } /* Comment.Multiline */
.cp { color: #0000ff } /* Comment.Preproc */
.cpf { color: #008000 } /* Comment.PreprocFile */
.c1 { color: #008000 } /* Comment.Single */
.cs { color: #008000 } /* Comment.Special */
.ge { font-style: italic } /* Generic.Emph */
.gh { font-weight: bold } /* Generic.Heading */
.gp { font-weight: bold } /* Generic.Prompt */
.gs { font-weight: bold } /* Generic.Strong */
.gu { font-weight: bold } /* Generic.Subheading */
.kc { color: #0000ff } /* Keyword.Constant */
.kd { color: #0000ff } /* Keyword.Declaration */
.kn { color: #0000ff } /* Keyword.Namespace */
.kp { color: #0000ff } /* Keyword.Pseudo */
.kr { color: #0000ff } /* Keyword.Reserved */
.kt { color: #2b91af } /* Keyword.Type */
.s { color: #a31515 } /* Literal.String */
.nc { color: #2b91af } /* Name.Class */
.ow { color: #0000ff } /* Operator.Word */
.sa { color: #a31515 } /* Literal.String.Affix */
.sb { color: #a31515 } /* Literal.String.Backtick */
.sc { color: #a31515 } /* Literal.String.Char */
.dl { color: #a31515 } /* Literal.String.Delimiter */
.sd { color: #a31515 } /* Literal.String.Doc */
.s2 { color: #a31515 } /* Literal.String.Double */
.se { color: #a31515 } /* Literal.String.Escape */
.sh { color: #a31515 } /* Literal.String.Heredoc */
.si { color: #a31515 } /* Literal.String.Interpol */
.sx { color: #a31515 } /* Literal.String.Other */
.sr { color: #a31515 } /* Literal.String.Regex */
.s1 { color: #a31515 } /* Literal.String.Single */
.ss { color: #a31515 } /* Literal.String.Symbol */
}

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,7 @@ $breakpoint-large: screen and (min-width: 60em)
:root {
--background-color: white;
--color: black;
--link-color: #cc3b95;
--link-color: #d12991;
--red: #c61d24;
--dim-color: #333;
@ -30,11 +30,9 @@ $breakpoint-large: screen and (min-width: 60em)
--main-background-color: #f8f8f8;
--main-background-color-transparent: rgba(#f8f8f8, 0);
--card-background: #e8e8e8;
--card-background-hover: #f0f0f0;
--dim-background: #f0f0f0;
--dim-background-transparent: rgba(#f0f0f0, 0);
--bg-1: #1f1f1f;
--bg-2: #2f2f2f;
--bg-3: #494949;
--forum-thread-read-color: #555;
--forum-thread-read-link-color: #888;
@ -55,8 +53,10 @@ $breakpoint-large: screen and (min-width: 60em)
@media (prefers-color-scheme: dark) {
:root {
--background-color: #2f2f2f;
--color: #eee;
--link-color: #cc3b95;
--link-color: #ff5dc2;
--color-error: #ff6666;
--dim-color: #bbb;
--dimmer-color: #999;
@ -78,8 +78,17 @@ $breakpoint-large: screen and (min-width: 60em)
--card-background-hover: #333;
--card-background-transparent: #242424D8;
--dim-background: #252525;
--dim-background-transparent: rgba(#252525, 0);
--bg-1: #1f1f1f;
--bg-2: #2f2f2f;
--bg-3: #494949;
--bg-4: #595959;
--bg-5: #cbcbcb;
--border-color: #595959;
--border-color-focused: #4e55ff;
--border-color-error: #ff3a3a;
--button-color-primary: #c900ea;
--forum-thread-read-color: #777;
--forum-thread-read-link-color: #999;

View File

@ -0,0 +1 @@
<svg viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M6,6l0,-6l2,0l0,6l6,0l0,2l-6,0l0,6l-2,0l0,-6l-6,0l0,-2l6,0Z"/></svg>

After

Width:  |  Height:  |  Size: 171 B

View File

@ -0,0 +1,7 @@
<?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 14 14" 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;">
<g transform="matrix(1,0,0,1,-5,-5)">
<path d="M16.004,9.414L7.397,18.021L5.983,16.607L14.589,8L7.004,8L7.004,6L18.004,6L18.004,17L16.004,17L16.004,9.414Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 656 B

View File

@ -1,11 +1 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 47.971 47.971">
<g>
<path d="M28.228,23.986L47.092,5.122c1.172-1.171,1.172-3.071,0-4.242c-1.172-1.172-3.07-1.172-4.242,0L23.986,19.744L5.121,0.88
c-1.172-1.172-3.07-1.172-4.242,0c-1.172,1.171-1.172,3.071,0,4.242l18.865,18.864L0.879,42.85c-1.172,1.171-1.172,3.071,0,4.242
C1.465,47.677,2.233,47.97,3,47.97s1.535-0.293,2.121-0.879l18.865-18.864L42.85,47.091c0.586,0.586,1.354,0.879,2.121,0.879
s1.535-0.293,2.121-0.879c1.172-1.171,1.172-3.071,0-4.242L28.228,23.986z"/>
</g>
</svg>
<svg viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M7,5.444l5.444,-5.444l1.556,1.556l-5.444,5.444l5.444,5.444l-1.556,1.556l-5.444,-5.444l-5.444,5.444l-1.556,-1.556l5.444,-5.444l-5.444,-5.444l1.556,-1.556l5.444,5.444Z"/></svg>

Before

Width:  |  Height:  |  Size: 776 B

After

Width:  |  Height:  |  Size: 257 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M17 6H22V8H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V8H2V6H7V3C7 2.44772 7.44772 2 8 2H16C16.5523 2 17 2.44772 17 3V6ZM18 8H6V20H18V8ZM13.4142 13.9997L15.182 15.7675L13.7678 17.1817L12 15.4139L10.2322 17.1817L8.81802 15.7675L10.5858 13.9997L8.81802 12.232L10.2322 10.8178L12 12.5855L13.7678 10.8178L15.182 12.232L13.4142 13.9997ZM9 4V6H15V4H9Z"></path></svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8.5 7C9.32843 7 10 6.32843 10 5.5C10 4.67157 9.32843 4 8.5 4C7.67157 4 7 4.67157 7 5.5C7 6.32843 7.67157 7 8.5 7ZM8.5 13.5C9.32843 13.5 10 12.8284 10 12C10 11.1716 9.32843 10.5 8.5 10.5C7.67157 10.5 7 11.1716 7 12C7 12.8284 7.67157 13.5 8.5 13.5ZM10 18.5C10 19.3284 9.32843 20 8.5 20C7.67157 20 7 19.3284 7 18.5C7 17.6716 7.67157 17 8.5 17C9.32843 17 10 17.6716 10 18.5ZM15.5 7C16.3284 7 17 6.32843 17 5.5C17 4.67157 16.3284 4 15.5 4C14.6716 4 14 4.67157 14 5.5C14 6.32843 14.6716 7 15.5 7ZM17 12C17 12.8284 16.3284 13.5 15.5 13.5C14.6716 13.5 14 12.8284 14 12C14 11.1716 14.6716 10.5 15.5 10.5C16.3284 10.5 17 11.1716 17 12ZM15.5 20C16.3284 20 17 19.3284 17 18.5C17 17.6716 16.3284 17 15.5 17C14.6716 17 14 17.6716 14 18.5C14 19.3284 14.6716 20 15.5 20Z"></path></svg>

After

Width:  |  Height:  |  Size: 859 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM8.82258 15.3427C8.42804 14.8663 7.9373 14.6957 7.34401 14.834L7.19355 14.875L6.60484 15.8911C6.37903 16.2863 6.5121 16.7903 6.90726 17.0161C7.26949 17.2231 7.7232 17.1286 7.97023 16.807L8.03226 16.7137L8.82258 15.3427ZM13.2097 8.66129C12.7218 9.06452 12.2298 10.2581 12.9194 11.4476L15.9597 16.7137C16.1895 17.1089 16.6895 17.2419 17.0847 17.0161C17.4469 16.8054 17.5889 16.3677 17.4361 15.9919L17.3871 15.8911L16.5847 14.5H17.7742C18.2298 14.5 18.5968 14.1331 18.5968 13.6774C18.5968 13.2568 18.2841 12.9118 17.8776 12.8612L17.7742 12.8548H15.6331L13.44 9.05741L13.2097 8.66129ZM13.4879 5.61694C13.1257 5.40995 12.672 5.50451 12.4249 5.82608L12.3629 5.91935L11.996 6.55242L11.6371 5.91935C11.4073 5.52419 10.9073 5.39113 10.5121 5.61694C10.1499 5.82762 10.0079 6.26532 10.1606 6.64118L10.2097 6.74194L11.0484 8.19758L8.3629 12.8508H6.26613C5.81048 12.8508 5.44355 13.2177 5.44355 13.6734C5.44355 14.094 5.7562 14.439 6.16268 14.4896L6.26613 14.496H13.746C14.0869 13.8562 13.6854 12.9472 12.9357 12.8579L12.8145 12.8508H10.2621L13.7903 6.74194C14.0161 6.34677 13.8831 5.84274 13.4879 5.61694Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12 11.3884C11.0942 9.62673 8.62833 6.34423 6.335 4.7259C4.13833 3.17506 3.30083 3.4434 2.75167 3.69256C2.11583 3.9784 2 4.95506 2 5.52839C2 6.10339 2.315 10.2367 2.52 10.9276C3.19917 13.2076 5.61417 13.9776 7.83917 13.7309C4.57917 14.2142 1.68333 15.4017 5.48083 19.6292C9.65833 23.9542 11.2058 18.7017 12 16.0392C12.7942 18.7017 13.7083 23.7651 18.4442 19.6292C22 16.0392 19.4208 14.2142 16.1608 13.7309C18.3858 13.9784 20.8008 13.2076 21.48 10.9276C21.685 10.2376 22 6.10256 22 5.52923C22 4.95423 21.8842 3.97839 21.2483 3.6909C20.6992 3.44256 19.8617 3.17423 17.665 4.72423C15.3717 6.34506 12.9058 9.62756 12 11.3884Z"></path></svg>

After

Width:  |  Height:  |  Size: 748 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M19.3034 5.33716C17.9344 4.71103 16.4805 4.2547 14.9629 4C14.7719 4.32899 14.5596 4.77471 14.411 5.12492C12.7969 4.89144 11.1944 4.89144 9.60255 5.12492C9.45397 4.77471 9.2311 4.32899 9.05068 4C7.52251 4.2547 6.06861 4.71103 4.70915 5.33716C1.96053 9.39111 1.21766 13.3495 1.5891 17.2549C3.41443 18.5815 5.17612 19.388 6.90701 19.9187C7.33151 19.3456 7.71356 18.73 8.04255 18.0827C7.41641 17.8492 6.82211 17.5627 6.24904 17.2231C6.39762 17.117 6.5462 17.0003 6.68416 16.8835C10.1438 18.4648 13.8911 18.4648 17.3082 16.8835C17.4568 17.0003 17.5948 17.117 17.7434 17.2231C17.1703 17.5627 16.576 17.8492 15.9499 18.0827C16.2789 18.73 16.6609 19.3456 17.0854 19.9187C18.8152 19.388 20.5875 18.5815 22.4033 17.2549C22.8596 12.7341 21.6806 8.80747 19.3034 5.33716ZM8.5201 14.8459C7.48007 14.8459 6.63107 13.9014 6.63107 12.7447C6.63107 11.5879 7.45884 10.6434 8.5201 10.6434C9.57071 10.6434 10.4303 11.5879 10.4091 12.7447C10.4091 13.9014 9.57071 14.8459 8.5201 14.8459ZM15.4936 14.8459C14.4535 14.8459 13.6034 13.9014 13.6034 12.7447C13.6034 11.5879 14.4323 10.6434 15.4936 10.6434C16.5442 10.6434 17.4038 11.5879 17.3825 12.7447C17.3825 13.9014 16.5548 14.8459 15.4936 14.8459Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M21.6634 9.98681L21.6354 9.91518L18.9164 2.8208C18.8612 2.68166 18.7634 2.56353 18.6371 2.48326C18.5098 2.40292 18.3607 2.36406 18.2104 2.37206C18.0601 2.38006 17.9159 2.43452 17.7979 2.52792C17.6809 2.62169 17.5966 2.75 17.5569 2.89453L15.7187 8.52037H8.28157L6.44336 2.89453C6.40366 2.75 6.31934 2.62169 6.20241 2.52792C6.08487 2.43389 5.94083 2.37903 5.79052 2.37102C5.64021 2.36301 5.49116 2.40226 5.36429 2.48326C5.2374 2.5632 5.13921 2.6814 5.08388 2.8208L2.36183 9.9245L2.33379 9.99512C1.94304 11.0182 1.895 12.1406 2.19691 13.1933C2.49882 14.2461 3.13436 15.1724 4.00794 15.8329L4.01832 15.8401L4.04221 15.8588L8.18917 18.9631L10.2393 20.5157L11.4856 21.4597C11.6319 21.5704 11.8104 21.6302 11.9939 21.6302C12.1774 21.6302 12.3559 21.5704 12.5023 21.4597L13.7486 20.5157L15.7997 18.9631L19.9706 15.8401L19.9819 15.8318C20.8585 15.1719 21.4966 14.2449 21.7999 13.1904C22.1033 12.1361 22.0553 11.0116 21.6634 9.98681Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M3.60972 1.81396L13.793 12L3.61082 22.1864C3.41776 22.1048 3.24866 21.962 3.13555 21.7667C3.0474 21.6144 3.00098 21.4416 3.00098 21.2656V2.73453C3.00098 2.32109 3.25188 1.96625 3.60972 1.81396ZM14.5 12.707L16.802 15.009L5.86498 21.342L14.5 12.707ZM17.699 9.50896L20.5061 11.1347C20.9841 11.4114 21.1473 12.0232 20.8705 12.5011C20.783 12.6523 20.6574 12.778 20.5061 12.8655L17.698 14.491L15.207 12L17.699 9.50896ZM5.86498 2.65796L16.803 8.98996L14.5 11.293L5.86498 2.65796Z"></path></svg>

After

Width:  |  Height:  |  Size: 599 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M15.001 17C10.8588 17 7.50098 13.6421 7.50098 9.5C7.50098 5.35786 10.8588 2 15.001 2C19.1431 2 22.501 5.35786 22.501 9.5C22.501 13.6421 19.1431 17 15.001 17ZM2.00098 2H6.00098V22H2.00098V2Z"></path></svg>

After

Width:  |  Height:  |  Size: 316 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12.0052 2C6.75435 2 2.44852 6.05 2.04102 11.1975L7.40102 13.4125C7.85518 13.1033 8.40352 12.9208 8.99435 12.9208C9.04685 12.9208 9.09852 12.9242 9.15102 12.9258L11.5352 9.47417V9.425C11.5352 7.34583 13.2252 5.655 15.3052 5.655C17.3835 5.655 19.0752 7.3475 19.0752 9.4275C19.0752 11.5075 17.3835 13.1983 15.3052 13.1983H15.2177L11.821 15.6242C11.821 15.6675 11.8243 15.7117 11.8243 15.7567C11.8243 17.3192 10.5618 18.5867 8.99935 18.5867C7.63685 18.5867 6.48602 17.6092 6.22352 16.3142L2.38602 14.725C3.57435 18.9225 7.42768 22 12.0052 22C17.5277 22 22.0043 17.5225 22.0043 12C22.0043 6.4775 17.5268 2 12.0052 2ZM7.07852 16.6667C7.29685 17.1192 7.67352 17.4992 8.17352 17.7083C9.25435 18.1575 10.501 17.645 10.9502 16.5625C11.1693 16.0375 11.1702 15.4633 10.9543 14.9383C10.7385 14.4133 10.3293 14.0042 9.80685 13.7858C9.28685 13.5692 8.73185 13.5783 8.24185 13.7608L9.51102 14.2858C10.3077 14.6192 10.6852 15.5358 10.3518 16.3317C10.021 17.1292 9.10435 17.5067 8.30685 17.175L7.07852 16.6667ZM17.8185 9.4225C17.8185 8.0375 16.691 6.91 15.306 6.91C13.9185 6.91 12.7935 8.0375 12.7935 9.4225C12.7935 10.81 13.9185 11.935 15.306 11.935C16.6918 11.935 17.8185 10.81 17.8185 9.4225ZM15.3118 7.53C16.3527 7.53 17.2002 8.375 17.2002 9.41833C17.2002 10.4608 16.3527 11.3058 15.3118 11.3058C14.2677 11.3058 13.4243 10.4608 13.4243 9.41833C13.4243 8.375 14.2685 7.53 15.3118 7.53Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M16 8.24537V15.5C16 19.0899 13.0899 22 9.5 22C5.91015 22 3 19.0899 3 15.5C3 11.9101 5.91015 9 9.5 9C10.0163 9 10.5185 9.06019 11 9.17393V12.3368C10.5454 12.1208 10.0368 12 9.5 12C7.567 12 6 13.567 6 15.5C6 17.433 7.567 19 9.5 19C11.433 19 13 17.433 13 15.5V2H16C16 4.76142 18.2386 7 21 7V10C19.1081 10 17.3696 9.34328 16 8.24537Z"></path></svg>

After

Width:  |  Height:  |  Size: 456 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M5.25098 3H18.751C19.993 3 21.001 4.00737 21.001 5.25V18.75C21.001 19.992 19.9935 21 18.751 21H5.25098C4.00898 21 3.00098 19.9925 3.00098 18.75V5.25C3.00098 4.008 4.00835 3 5.25098 3ZM13.171 6.42054V12.1795C13.171 12.7762 13.6545 13.26 14.2507 13.26H17.5812C18.1776 13.26 18.661 12.7765 18.661 12.1795V6.42054C18.661 5.82384 18.1774 5.34 17.5812 5.34H14.2507C13.6543 5.34 13.171 5.82348 13.171 6.42054ZM5.34098 6.42045V16.6796C5.34098 17.2762 5.82455 17.76 6.42071 17.76H9.75125C10.3476 17.76 10.831 17.277 10.831 16.6796V6.42045C10.831 5.82375 10.3474 5.34 9.75125 5.34H6.42071C5.82428 5.34 5.34098 5.82303 5.34098 6.42045Z"></path></svg>

After

Width:  |  Height:  |  Size: 751 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M21.001 3V14.7391L16.3053 19.4348H12.3923L9.95523 21.7826H6.91402V19.4348H3.00098V6.13043L4.2281 3H21.001ZM19.4358 4.56522H6.13141V16.3043H9.26185V18.6522L11.6097 16.3043H16.3053L19.4358 13.1739V4.56522ZM16.3053 7.69565V12.3913H14.7401V7.69565H16.3053ZM12.3923 7.69565V12.3913H10.8271V7.69565H12.3923Z"></path></svg>

After

Width:  |  Height:  |  Size: 428 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M18.2048 2.25H21.5128L14.2858 10.51L22.7878 21.75H16.1308L10.9168 14.933L4.95084 21.75H1.64084L9.37084 12.915L1.21484 2.25H8.04084L12.7538 8.481L18.2048 2.25ZM17.0438 19.77H18.8768L7.04484 4.126H5.07784L17.0438 19.77Z"></path></svg>

After

Width:  |  Height:  |  Size: 344 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M22.2125 5.65605C21.4491 5.99375 20.6395 6.21555 19.8106 6.31411C20.6839 5.79132 21.3374 4.9689 21.6493 4.00005C20.8287 4.48761 19.9305 4.83077 18.9938 5.01461C18.2031 4.17106 17.098 3.69303 15.9418 3.69434C13.6326 3.69434 11.7597 5.56661 11.7597 7.87683C11.7597 8.20458 11.7973 8.52242 11.8676 8.82909C8.39047 8.65404 5.31007 6.99005 3.24678 4.45941C2.87529 5.09767 2.68005 5.82318 2.68104 6.56167C2.68104 8.01259 3.4196 9.29324 4.54149 10.043C3.87737 10.022 3.22788 9.84264 2.64718 9.51973C2.64654 9.5373 2.64654 9.55487 2.64654 9.57148C2.64654 11.5984 4.08819 13.2892 6.00199 13.6731C5.6428 13.7703 5.27232 13.8194 4.90022 13.8191C4.62997 13.8191 4.36771 13.7942 4.11279 13.7453C4.64531 15.4065 6.18886 16.6159 8.0196 16.6491C6.53813 17.8118 4.70869 18.4426 2.82543 18.4399C2.49212 18.4402 2.15909 18.4205 1.82812 18.3811C3.74004 19.6102 5.96552 20.2625 8.23842 20.2601C15.9316 20.2601 20.138 13.8875 20.138 8.36111C20.138 8.1803 20.1336 7.99886 20.1256 7.81997C20.9443 7.22845 21.651 6.49567 22.2125 5.65605Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M1.17444 8.30112C0.892531 7.88758 0.922443 7.88758 1.50184 7.37867C2.73388 6.29651 3.8964 5.11316 5.23826 4.1668C6.45346 3.31485 8.06417 2.7652 9.16466 4.1202C10.1791 5.36886 10.203 7.26157 10.4599 8.76952C10.7168 10.3336 10.963 11.9336 11.5114 13.4308C11.6632 13.8514 11.9535 14.6472 12.4793 14.7141C13.1568 14.8073 13.8474 13.6184 14.1617 13.1739C14.979 11.9945 16.0867 10.4053 15.9469 8.88781C15.8094 7.27591 14.0685 7.57941 12.9811 7.96416C13.1556 6.1551 14.8392 4.1214 16.4607 3.43314C18.1801 2.71979 20.7372 2.73174 21.6011 4.67105C22.5235 6.77286 21.6943 9.21402 20.6894 11.1187C19.5925 13.187 18.1801 15.1012 16.671 16.8888C15.3399 18.4768 13.765 20.2189 11.7803 20.9777C9.51357 21.8416 8.17052 20.158 7.39862 18.2079C6.55622 16.0846 6.13682 13.702 5.52862 11.4915C5.27291 10.5571 4.96941 9.49362 4.3624 8.72292C3.57019 7.72757 2.67044 8.66317 1.88779 9.19968C1.61894 8.93322 1.39669 8.59267 1.17444 8.30112Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM9.71002 19.6674C8.74743 17.6259 8.15732 15.3742 8.02731 13H4.06189C4.458 16.1765 6.71639 18.7747 9.71002 19.6674ZM10.0307 13C10.1811 15.4388 10.8778 17.7297 12 19.752C13.1222 17.7297 13.8189 15.4388 13.9693 13H10.0307ZM19.9381 13H15.9727C15.8427 15.3742 15.2526 17.6259 14.29 19.6674C17.2836 18.7747 19.542 16.1765 19.9381 13ZM4.06189 11H8.02731C8.15732 8.62577 8.74743 6.37407 9.71002 4.33256C6.71639 5.22533 4.458 7.8235 4.06189 11ZM10.0307 11H13.9693C13.8189 8.56122 13.1222 6.27025 12 4.24799C10.8778 6.27025 10.1811 8.56122 10.0307 11ZM14.29 4.33256C15.2526 6.37407 15.8427 8.62577 15.9727 11H19.9381C19.542 7.8235 17.2836 5.22533 14.29 4.33256Z"></path></svg>

After

Width:  |  Height:  |  Size: 891 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12.2439 4C12.778 4.00294 14.1143 4.01586 15.5341 4.07273L16.0375 4.09468C17.467 4.16236 18.8953 4.27798 19.6037 4.4755C20.5486 4.74095 21.2913 5.5155 21.5423 6.49732C21.942 8.05641 21.992 11.0994 21.9982 11.8358L21.9991 11.9884L21.9991 11.9991C21.9991 11.9991 21.9991 12.0028 21.9991 12.0099L21.9982 12.1625C21.992 12.8989 21.942 15.9419 21.5423 17.501C21.2878 18.4864 20.5451 19.261 19.6037 19.5228C18.8953 19.7203 17.467 19.8359 16.0375 19.9036L15.5341 19.9255C14.1143 19.9824 12.778 19.9953 12.2439 19.9983L12.0095 19.9991L11.9991 19.9991C11.9991 19.9991 11.9956 19.9991 11.9887 19.9991L11.7545 19.9983C10.6241 19.9921 5.89772 19.941 4.39451 19.5228C3.4496 19.2573 2.70692 18.4828 2.45587 17.501C2.0562 15.9419 2.00624 12.8989 2 12.1625V11.8358C2.00624 11.0994 2.0562 8.05641 2.45587 6.49732C2.7104 5.51186 3.45308 4.73732 4.39451 4.4755C5.89772 4.05723 10.6241 4.00622 11.7545 4H12.2439ZM9.99911 8.49914V15.4991L15.9991 11.9991L9.99911 8.49914Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -106,6 +106,9 @@ func ProjectAndStuffToTemplate(p *hmndata.ProjectAndStuff, url string, theme str
for _, o := range p.Owners {
res.Owners = append(res.Owners, UserToTemplate(o, theme))
}
if p.HeaderImage != nil {
res.HeaderImage = hmnurl.BuildS3Asset(p.HeaderImage.S3Key)
}
return res
}
@ -129,7 +132,7 @@ func ProjectToProjectSettings(
p *models.Project,
owners []*models.User,
tag string,
lightLogoUrl, darkLogoUrl string,
lightLogo, darkLogo, headerImage *models.Asset,
currentTheme string,
) ProjectSettings {
ownerUsers := make([]User, 0, len(owners))
@ -147,8 +150,26 @@ func ProjectToProjectSettings(
Blurb: p.Blurb,
Description: p.Description,
Owners: ownerUsers,
LightLogo: lightLogoUrl,
DarkLogo: darkLogoUrl,
LightLogo: AssetToTemplate(lightLogo),
DarkLogo: AssetToTemplate(darkLogo),
HeaderImage: AssetToTemplate(headerImage),
}
}
func AssetToTemplate(a *models.Asset) *Asset {
if a == nil {
return nil
}
return &Asset{
Url: hmnurl.BuildS3Asset(a.S3Key),
ID: a.ID.String(),
Filename: a.Filename,
Size: a.Size,
MimeType: a.MimeType,
Width: a.Width,
Height: a.Height,
}
}
@ -236,77 +257,102 @@ type LinkService struct {
}
var LinkServices = []LinkService{
// {
// Name: "itch.io",
// IconName: "itch",
// Regex: regexp.MustCompile(`://(?P<username>[\w-]+)\.itch\.io`),
// },
{
Name: "YouTube",
IconName: "youtube",
Regex: regexp.MustCompile(`youtube\.com/(c/)?(?P<userdata>[\w/-]+)$`),
Name: "App Store",
IconName: "app-store",
Regex: regexp.MustCompile(`^https?://apps.apple.com`),
},
{
Name: "Twitter",
IconName: "twitter",
Regex: regexp.MustCompile(`twitter\.com/(?P<userdata>\w+)$`),
Name: "Bluesky",
IconName: "bluesky",
Regex: regexp.MustCompile(`^https?://bsky.app/profile/(?P<username>[\w.-]+)$`),
},
{
Name: "Discord",
IconName: "discord",
Regex: regexp.MustCompile(`^https?://discord\.gg`),
},
{
Name: "GitHub",
IconName: "github",
Regex: regexp.MustCompile(`github\.com/(?P<userdata>[\w/-]+)$`),
Regex: regexp.MustCompile(`^https?://github\.com/(?P<username>[\w/-]+)`),
},
{
Name: "Twitch",
IconName: "twitch",
Regex: regexp.MustCompile(`twitch\.tv/(?P<userdata>[\w/-]+)$`),
Name: "GitLab",
IconName: "gitlab",
Regex: regexp.MustCompile(`^https?://gitlab\.com/(?P<username>[\w/-]+)`),
},
{
Name: "Hitbox",
IconName: "hitbox",
Regex: regexp.MustCompile(`hitbox\.tv/(?P<userdata>[\w/-]+)$`),
Name: "Google Play",
IconName: "google-play",
Regex: regexp.MustCompile(`^https?://play\.google\.com`),
},
{
Name: "Patreon",
IconName: "patreon",
Regex: regexp.MustCompile(`patreon\.com/(?P<userdata>[\w/-]+)$`),
Regex: regexp.MustCompile(`^https?://patreon\.com/(?P<username>[\w-]+)`),
},
{
Name: "SoundCloud",
IconName: "soundcloud",
Regex: regexp.MustCompile(`soundcloud\.com/(?P<userdata>[\w/-]+)$`),
Name: "Twitch",
IconName: "twitch",
Regex: regexp.MustCompile(`^https?://twitch\.tv/(?P<username>[\w/-]+)`),
},
{
Name: "itch.io",
IconName: "itch",
Regex: regexp.MustCompile(`(?P<userdata>[\w/-]+)\.itch\.io/?$`),
Name: "Twitter",
IconName: "twitter",
Regex: regexp.MustCompile(`^https?://(twitter|x)\.com/(?P<username>\w+)`),
},
{
Name: "Vimeo",
IconName: "vimeo",
Regex: regexp.MustCompile(`^https?://vimeo\.com/(?P<username>\w+)`),
},
{
Name: "YouTube",
IconName: "youtube",
Regex: regexp.MustCompile(`youtube\.com/(c/)?(?P<username>[@\w/-]+)$`),
},
}
func ParseKnownServicesForLink(link *models.Link) (service LinkService, userData string) {
func ParseKnownServicesForLink(link *models.Link) (service LinkService, username string) {
for _, svc := range LinkServices {
match := svc.Regex.FindStringSubmatch(link.URL)
if match != nil {
return svc, match[svc.Regex.SubexpIndex("userdata")]
username := ""
if idx := svc.Regex.SubexpIndex("username"); idx >= 0 {
username = match[idx]
}
return svc, username
}
}
return LinkService{}, ""
return LinkService{
IconName: "website",
}, ""
}
func LinkToTemplate(link *models.Link) Link {
tlink := Link{
Name: link.Name,
Url: link.URL,
LinkText: link.URL,
service, username := ParseKnownServicesForLink(link)
return Link{
Name: link.Name,
Url: link.URL,
ServiceName: service.Name,
Icon: service.IconName,
Username: username,
}
}
service, userData := ParseKnownServicesForLink(link)
if tlink.Name == "" && service.Name != "" {
tlink.Name = service.Name
func LinksToTemplate(links []*models.Link) []Link {
res := make([]Link, len(links))
for i, link := range links {
res[i] = LinkToTemplate(link)
}
if service.IconName != "" {
tlink.Icon = service.IconName
}
if userData != "" {
tlink.LinkText = userData
}
return tlink
return res
}
func TimelineItemsToJSON(items []TimelineItem) string {

View File

@ -23,7 +23,7 @@
<div class="ph3 ph0-ns mt4">
<h2>Leadership</h2>
<div class="flex flex-column flex-row-ns g3">
<div class="flex-fair pa3 bg--dim br3">
<div class="flex-fair pa3 bg3 br3">
<h3>Ben Visness</h3>
<h4>Lead<!--, Foundation President--></h4>
<p>
@ -40,7 +40,7 @@
</p> -->
</div>
<div class="flex-fair pa3 bg--dim br3">
<div class="flex-fair pa3 bg3 br3">
<h3>Asaf Gartner</h3>
<h4>Admin<!--, Foundation Secretary--></h4>
<p>
@ -51,7 +51,7 @@
</p>
</div>
<div class="flex-fair pa3 bg--dim br3">
<div class="flex-fair pa3 bg3 br3">
<h3>Colin Davidson</h3>
<h4>Admin<!--, Foundation Treasurer--></h4>
<p>
@ -67,7 +67,7 @@
<div class="ph3 ph0-ns mt4">
<h2>Key Contributors</h2>
<div class="flex flex-column flex-row-ns g3">
<div class="flex-fair pa3 bg--dim br3">
<div class="flex-fair pa3 bg3 br3">
<h3>Martin Fouilleul</h3>
<h4>Orca Lead</h4>
<p>Martin is a systems programmer, researcher, and PhD with experience in programming languages and models for distributed temporal interactions in music and performing arts software. He is also a former sound engineer and computer music designer.</p>

View File

@ -60,7 +60,7 @@
{{ if .ShowEduOptions }}
{{/* Hope you have a .Article field! */}}
<div class="bg--dim br3 pa3 mt3">
<div class="bg3 br3 pa3 mt3">
<h4>Education Options</h4>
<div class="mb2">
<label for="slug">Slug:</label>
@ -86,7 +86,7 @@
</div>
{{ end }}
</form>
<div id="preview-container" class="post post-preview mathjax flex-fair-ns overflow-auto mv3 mv0-ns ml3-ns pa3 br3 bg--dim {{ .PreviewClass }}">
<div id="preview-container" class="post post-preview mathjax flex-fair-ns overflow-auto mv3 mv0-ns ml3-ns pa3 br3 bg3 {{ .PreviewClass }}">
<div id="preview" class="post-content"></div>
</div>
<input type="file" multiple name="file_input" id="file_input" class="dn" />{{/* NOTE(asaf): Placing this outside the form to avoid submitting it to the server by accident */}}

View File

@ -3,7 +3,7 @@
{{ define "content" }}
<div class="mw7 m--center">
<h3 class="mb3">Are you sure you want to delete this article?</h3>
<div class="bg--dim pa3 br3 tl post-content">
<div class="bg3 pa3 br3 tl post-content">
<h1>{{ .Article.Title }}</h1>
{{ .Article.Content }}
</div>

View File

@ -7,7 +7,7 @@
<p>Dive into one of these topics and start learning.</p>
<div class="flex flex-column flex-row-ns g3 mt3 mb4">
<a href="#compilers" class="edu-topic db flex-fair-ns bg--dim br3 overflow-hidden c--inherit flex flex-column">
<a href="#compilers" class="edu-topic db flex-fair-ns bg3 br3 overflow-hidden c--inherit flex flex-column">
<img src="{{ static "education/compilers.jpg" }}">
<div class="pa3">
<h2>Compilers</h2>
@ -16,7 +16,7 @@
</div>
</div>
</a>
<a href="#networking" class="edu-topic db flex-fair-ns bg--dim br3 overflow-hidden c--inherit flex flex-column">
<a href="#networking" class="edu-topic db flex-fair-ns bg3 br3 overflow-hidden c--inherit flex flex-column">
<img src="{{ static "education/networking.jpg" }}">
<div class="pa3">
<h2>Networking</h2>
@ -25,7 +25,7 @@
</div>
</div>
</a>
<a href="#time" class="edu-topic db flex-fair-ns bg--dim br3 overflow-hidden c--inherit flex flex-column">
<a href="#time" class="edu-topic db flex-fair-ns bg3 br3 overflow-hidden c--inherit flex flex-column">
<img src="{{ static "education/time.jpg" }}">
<div class="pa3">
<h2>Time</h2>
@ -39,17 +39,17 @@
<h2>What makes us different?</h2>
<div class="flex flex-column flex-row-ns g3 mb4">
<div class="flex-fair bg--dim pa3 br2">
<div class="flex-fair bg3 pa3 br2">
<h3>Real material.</h3>
We equip you to go straight to the source. Our guides are structured around books and articles written by experts. We give you high-quality material to read, and the context to understand it. You do the rest.
</div>
<div class="flex-fair bg--dim pa3 br3">
<div class="flex-fair bg3 pa3 br3">
<h3>For any skill level.</h3>
Each guide runs the gamut from beginner to advanced. Whether you're new to a topic or have been practicing it for years, read through our guides and you'll find something new.
</div>
<div class="flex-fair bg--dim pa3 br3">
<div class="flex-fair bg3 pa3 br3">
<h3>Designed for programmers.</h3>
We're not here to teach you how to program. We're here to teach you a specific topic.
@ -59,7 +59,7 @@
<h2>All Topics</h2>
{{ range .Courses }}
<div id="{{ .Slug }}" class="edu-course mv3 bg--dim pa3 br3">
<div id="{{ .Slug }}" class="edu-course mv3 bg3 pa3 br3">
<h3>{{ .Name }}</h3>
<div class="overflow-hidden">
<div class="edu-articles ml3 pl3">

View File

@ -10,7 +10,7 @@
<div class="mt3 mw7">
{{ range .Fishbowls }}
<div class="br2 bg--dim pa3 mb2">
<div class="br2 bg3 pa3 mb2">
{{ if .Valid }}
<a href="{{ .Url }}"><h3 class="f4 ma0">{{ .Fishbowl.Title }}</h3></a>
{{ else }}

View File

@ -1,4 +1,4 @@
<div class="bg--dim pa3 br3 tl">
<div class="bg3 pa3 br3 tl">
<div class="w-100 flex items-center">
<div class="w-20 mw3 w3">
<!-- Mobile avatar -->

View File

@ -1,11 +1,16 @@
{{/* NOTE(asaf): Make sure to include js/image_selector.js */}}
<input {{ if .Required }}required{{ end }} class="image_input" type="file" accept="image/*" name="{{ .Name }}" />
<input class="remove_input" type="hidden" name="remove_{{ .Name }}" />
<input {{ if .Required }}required{{ end }} class="imginput dn" type="file" accept="image/*" name="{{ .Name }}" />
<input class="imginput-remove" type="hidden" name="remove_{{ .Name }}" />
<span class="error dn notice notice-failure mv2 mw6"></span>
<div class="image_container">
<img class="mw6" data-original="{{ .Src }}" src="{{ .Src }}" />
<div >
<a href="javascript:;" class="reset">Reset</a>
<a href="javascript:;" class="remove">Remove</a>
<div class="imginput-container pa3 flex g3 items-center">
{{ $url := or (and .Asset .Asset.Url) "" }}
{{ $filename := or (and .Asset .Asset.Filename) "" }}
<img class="w4 flex-shrink-0" data-imginput-original="{{ $url }}" data-imginput-original-filename="{{ $filename }}" src="{{ $url }}" />
<div class="w1 flex-grow-1 flex flex-column g1">
<div class="flex f6">
<a href="javascript:;" class="imginput-reset-link">Reset</a>
<a href="javascript:;" class="imginput-remove-link">Remove</a>
</div>
<div class="imginput-filename b truncate lh-title"></div>
</div>
</div>

View File

@ -1,11 +1,11 @@
{{- /*gotype: git.handmade.network/hmn/hmn/src/templates.ProjectCardData*/ -}}
<a class="project-card flex br2 overflow-hidden items-center relative {{ .Classes }}" href="{{ .Project.Url }}" >
<a class="project-card pa3 flex g3 br2 overflow-hidden relative {{ .Classes }}" href="{{ .Project.Url }}" >
{{ with .Project.Logo }}
<div class="image-container flex-shrink-0">
<div class="image bg-center cover" style="background-image:url({{ . }})"></div>
<div class="image-container flex-shrink-0 aspect-ratio">
<div class="image bg-center cover aspect-ratio--1x1" style="background-image:url({{ . }})"></div>
</div>
{{ end }}
<div class="details pa3 flex-grow-1">
<div class="details flex-grow-1">
<h3 class="b mb2 f4">{{ .Project.Name }}</h3>
<div class="blurb">{{ .Project.Blurb }}</div>
<hr>

View File

@ -1,5 +1,5 @@
<a href="{{ .Url }}">
<div class="role pa3 bg--dim br3">
<div class="role pa3 bg3 br3">
<h2>{{ .Name }}</h2>
<div class="c--normal">
{{ .Description }}

View File

@ -33,6 +33,30 @@
<link rel="stylesheet" type="text/css" href="{{ static "style.css" }}">
<script src="{{ static "js/script.js" }}"></script>
{{ if .EsBuildSSEUrl }}
<script>
new EventSource("{{ .EsBuildSSEUrl }}").addEventListener('change', e => {
const { added, removed, updated } = JSON.parse(e.data)
console.log("EsBuild", added, removed, updated);
if (!added.length && !removed.length && updated.length === 1) {
for (const link of document.getElementsByTagName("link")) {
const url = new URL(link.href)
if (url.host === location.host && url.pathname === updated[0]) {
const next = link.cloneNode()
next.href = updated[0] + '?' + Math.random().toString(36).slice(2)
next.onload = () => link.remove()
link.parentNode.insertBefore(next, link.nextSibling)
return
}
}
}
})
</script>
{{ end }}
{{ template "extrahead" . }}
</head>

View File

@ -1,4 +1,4 @@
{{ template "base.html" . }}
{{ template "base-2024.html" . }}
{{ define "extrahead" }}
{{ template "markdown_previews.html" .TextEditor }}
@ -22,212 +22,208 @@
{{ end }}
{{ define "content" }}
<div class="ph3 ph0-ns">
{{ if .Editing }}
<h1>Edit {{ .ProjectSettings.Name }}</h1>
{{ else }}
<h1>Create a new {{ if .ProjectSettings.JamParticipation }}jam {{ end }}project</h1>
{{ end }}
<form id="project_form" class="tabbed edit-form" method="POST" enctype="multipart/form-data">
<div class="bg1 flex bb">
<form id="project_form" class="hmn-form pa4 flex-fair" method="POST" enctype="multipart/form-data">
{{ csrftoken .Session }}
<div class="tab" data-name="General" data-slug="general">
<div class="edit-form-row">
<div class="pt-input-ns">Project name:</div>
<div>
<input required type="text" name="project_name" maxlength="255" class="textbox" value="{{ .ProjectSettings.Name }}">
<span class="note">* Required</span>
<h1 class="f3">
{{ if .Editing }}
Edit {{ .ProjectSettings.Name }}
{{ else }}
Create a {{ if .ProjectSettings.JamParticipation }}jam {{ end }}project
{{ end }}
</h1>
<hr class="mv3">
<div class="flex flex-column g3">
<div class="input-group">
<label>Project Title*</label>
<input required type="text" name="project_name" maxlength="255" class="textbox" value="{{ .ProjectSettings.Name }}">
</div>
<div class="input-group">
<label>Short Description*</label>
<textarea id="description" required maxlength="140" name="shortdesc">
{{- .ProjectSettings.Blurb -}}
</textarea>
<div class="f6">Plaintext only. No links or markdown.</div>
</div>
<div class="input-group">
<label>Long Description</label>
<textarea id="full_description" class="w-100 h5 minh-5 mono lh-copy" name="full_description">
{{- .ProjectSettings.Description -}}
</textarea>
<div class="flex justify-end items-center f6">
<div class="upload_bar flex-grow-1">
<div class="instructions">
Upload files by dragging & dropping, pasting, or <label class="pointer link" for="file_input">selecting</label> them.
</div>
<div class="progress flex">
<div class="progress_text mr3"></div>
<div class="progress_bar flex-grow-1 flex-shrink-1 pa1"><div class=""></div></div>
</div>
</div>
</div>
</div>
<div class="edit-form-row">
<div class="pt-input-ns">Status:</div>
<div>
<select name="lifecycle">
<option value="active" {{ if eq .ProjectSettings.Lifecycle "active" }}selected{{ end }}>Active</option>
<option value="hiatus" {{ if eq .ProjectSettings.Lifecycle "hiatus" }}selected{{ end }}>On Hiatus</option>
<option value="done" {{ if eq .ProjectSettings.Lifecycle "done" }}selected{{ end }}>Completed</option>
<option value="dead" {{ if eq .ProjectSettings.Lifecycle "dead" }}selected{{ end }}>Abandoned</option>
</select>
</div>
<div class="input-group">
<label>Status</label>
<select name="lifecycle">
<option value="active" {{ if eq .ProjectSettings.Lifecycle "active" }}selected{{ end }}>Active</option>
<option value="hiatus" {{ if eq .ProjectSettings.Lifecycle "hiatus" }}selected{{ end }}>On Hiatus</option>
<option value="done" {{ if eq .ProjectSettings.Lifecycle "done" }}selected{{ end }}>Completed</option>
<option value="dead" {{ if eq .ProjectSettings.Lifecycle "dead" }}selected{{ end }}>Abandoned</option>
</select>
</div>
<div class="edit-form-row">
<div class="pt-input-ns">Owners:</div>
<div>
<input id="owner_name" form="" type="text" placeholder="Enter another user's username" />
<a href="javascript:;" id="owner_add" class="">Add</a>
<span id="owners_error" class="note"></span>
<div id="owner_list" class="pt1">
<fieldset>
<legend>Discord Tag</legend>
<div class="pa3 input-group">
<input
id="tag" name="tag" type="text"
pattern="^[a-z0-9]+(-[a-z0-9]+)*$" maxlength="20"
value="{{ .ProjectSettings.Tag }}"
>
<div class="f6 mt1" id="tag-discord-info">If you have linked your Discord account, any #project-showcase messages with the tag "&amp;<span id="tag-preview"></span>" will automatically be associated with this project.</div>
<div class="f6">Tags must be all lowercase, and can use hyphens to separate words.</div>
</div>
</fieldset>
<fieldset>
<legend class="flex justify-between">
<span>Project Logo</span>
<a href="#" class="normal" onclick="openLogoSelector(event)">+ Upload Project Logo</a>
</legend>
<div class="light_logo">
{{ template "image_selector.html" imageselectordata "light_logo" .ProjectSettings.LightLogo false }}
<div class="show-when-sibling-hidden flex justify-center items-center f6 pa2">Images should be square, and at least 256x256.</div>
</div>
</fieldset>
<fieldset>
<legend class="flex justify-between">
<span>Header Image</span>
<a href="#" class="normal" onclick="openHeaderSelector(event)">+ Upload Header Image</a>
</legend>
<div class="header_image">
{{ template "image_selector.html" imageselectordata "header_image" .ProjectSettings.HeaderImage false }}
<div class="show-when-sibling-hidden flex justify-center items-center f6 pa2">Images should be very beeg (TODO)</div>
</div>
</fieldset>
<fieldset>
<legend>Owners</legend>
<div class="pa3">
<div class="flex">
<input class="flex-grow-1 no-border bl bt bb br-0" id="owner_name" type="text" placeholder="Enter a username" />
<button class="flex no-padding no-border pa3 bt br bb bl-0 also-focus" id="owner_add"><span class="flex w1">{{ svg "add" }}</span></button>
</div>
<div id="owners_error" class="f6"></div>
<div id="owner_list" class="pt3 flex flex-wrap g3">
<template id="owner_row">
<div class="owner_row flex flex-row bg--card w5 pv1 ph2" data-tmpl="root">
<div class="owner_row flex flex-row items-center bg3 pa2" data-tmpl="root">
<input type="hidden" name="owners" data-tmpl="input" />
<span class="flex-grow-1" data-tmpl="name"></span>
<a class="remove_owner" href="javascript:;">X</a>
<span data-tmpl="name"></span>
<a class="remove_owner svgicon f7 link--normal pl2" href="javascript:;">{{ svg "close" }}</a>
</div>
</template>
{{ range .ProjectSettings.Owners }}
<div class="owner_row flex flex-row bg--card w5 pv1 ph2">
<div class="owner_row flex flex-row items-center bg3 pa2">
<input type="hidden" name="owners" value="{{ .Username }}" />
<span class="flex-grow-1">{{ .Username }}</span>
<span>{{ .Username }}</span>
{{ if (or $.User.IsStaff (ne .ID $.User.ID)) }}
<a class="remove_owner" href="javascript:;">X</a>
<a class="remove_owner svgicon f7 link--normal pl2" href="javascript:;">{{ svg "close" }}</a>
{{ end }}
</div>
{{ end }}
</div>
</div>
</div>
<div class="edit-form-row">
<div class="pt-input-ns">Tag:</div>
<div>
<input
id="tag" name="tag" type="text"
pattern="^[a-z0-9]+(-[a-z0-9]+)*$" maxlength="20"
value="{{ .ProjectSettings.Tag }}"
/>
<div class="c--dim f7 mt1">e.g. "imgui" or "text-editor". Tags must be all lowercase, and can use hyphens to separate words.</div>
<div class="c--dim f7" id="tag-discord-info">If you have linked your Discord account, any #project-showcase messages with the tag "&amp;<span id="tag-preview"></span>" will automatically be associated with this project.</div>
</div>
</div>
</fieldset>
{{ if .ProjectSettings.JamParticipation }}
<div class="edit-form-row">
<div class="pt-input-ns">Jam Participation</div>
<div class="pt-input-ns">
<fieldset>
<legend>Jam Participation</legend>
<div class="pa3 flex flex-column g2">
{{ range .ProjectSettings.JamParticipation }}
<div class="pb1">
<div>
<input id="jam_{{ .JamSlug }}" type="checkbox" name="jam_participation" value="{{ .JamSlug }}" {{ if .Participating }}checked{{ end }} />
<label for="jam_{{ .JamSlug }}">{{ .JamName }}</label>
</div>
{{ end }}
</div>
</div>
</fieldset>
{{ end }}
<fieldset>
<legend class="flex justify-between">
<span>Links</span>
<a href="#" class="normal" onclick="addLink(event)">+ Add Link</a>
</legend>
<div class="pa3 input-group">
<div id="links" class="flex flex-column g3 relative">
</div>
<template id="link_row">
<div class="link_row w-100 flex flex-row items-center" data-tmpl="root">
<span class="link_handle svgicon pr3 pointer grab" onmousedown="startLinkDrag(event)">{{ svg "draggable" }}</span>
<input data-tmpl="nameInput" class="link_name mr3 w4" type="text" placeholder="Name" />
<input data-tmpl="urlInput" class="link_url flex-grow-1" type="url" placeholder="Link" />
<a class="delete_link svgicon link--normal pl3 f3" href="javascript:;" onclick="deleteLink(event)">{{ svg "delete" }}</a>
</div>
</template>
<template id="link_row_dummy">
<div class="link_row_dummy flex flex-row" data-tmpl="root">
<input class="o-0">
</div>
</template>
</div>
<input id="links_json" type="hidden" name="links">
</fieldset>
{{ if and .Editing .User.IsStaff }}
<div class="edit-form-row">
<div class="pt-input-ns">Admin settings</div>
</div>
<div class="edit-form-row">
<div>Official:</div>
<div>
<input id="official" type="checkbox" name="official" {{ if not .ProjectSettings.Personal }}checked{{ end }} />
<label for="official">Official HMN project</label>
</div>
</div>
<div class="edit-form-row">
<div>Hidden:</div>
<div>
<input id="hidden" type="checkbox" name="hidden" {{ if .ProjectSettings.Hidden }}checked{{ end }} />
<label for="hidden">Hide</label>
</div>
</div>
<div class="edit-form-row">
<div class="pt-input-ns">Slug:</div>
<div>
<input type="text" name="slug" maxlength="255" class="textbox" value="{{ .ProjectSettings.Slug }}">
<div class="c--dim f7 mt1">Has no effect for personal projects. Personal projects have a slug derived from the title.</div>
<div class="c--dim f7">If you change this, make sure to change DNS too!</div>
</div>
</div>
<div class="edit-form-row">
<div>Featured:</div>
<div>
<input id="featured" type="checkbox" name="featured" {{ if .ProjectSettings.Featured }}checked{{ end }} />
<label for="featured">Featured</label>
<div class="c--dim f7">Bump to the top of the project index and show in the carousel. Has no effect for personal projects.</div>
</div>
</div>
{{ end }}
<div class="edit-form-row">
<div></div>
<div>
{{ if .Editing }}
<input type="submit" value="Save" />
{{ else }}
<a class="button submit" href="javascript:;" onclick="gotoTab('description');">Next</a>
{{ end }}
</div>
</div>
</div>
<div class="tab" data-name="Description" data-slug="description">
<div class="edit-form-row">
<div class="pt-input-ns">Short description:</div>
<div>
<textarea id="description" required maxlength="140" name="shortdesc">
{{- .ProjectSettings.Blurb -}}
</textarea>
<div class="c--dim f7">Plaintext only. No links or markdown.</div>
</div>
</div>
<div class="edit-form-row">
<div class="pt-input-ns">Project links:</div>
<div>
<textarea class="links" name="links" id="links" maxlength="2048" data-max-chars="2048">
{{- .ProjectSettings.LinksText -}}
</textarea>
<div class="c--dim f7">
<div>Relevant links to put on the project page.</div>
<div>Format: url [Title] (e.g. <code>http://example.com/ Example Site</code>)</div>
<div>(1 per line, 10 max)</div>
</div>
</div>
</div>
<div class="edit-form-row">
<div class="pt-input-ns">Full description:</div>
<div>
<textarea id="full_description" class="w-100 h5 minh-5 mono lh-copy" name="full_description">
{{- .ProjectSettings.Description -}}
</textarea>
<div class="flex justify-end items-center mt2">
<div class="upload_bar flex-grow-1">
<div class="instructions">
Upload files by dragging & dropping, pasting, or <label class="pointer link" for="file_input">selecting</label> them.
</div>
<div class="progress flex">
<div class="progress_text mr3"></div>
<div class="progress_bar flex-grow-1 flex-shrink-1 pa1"><div class=""></div></div>
</div>
<fieldset>
<legend>Admin Properties</legend>
<div class="pa3 flex flex-column g3">
<div class="flex flex-column g2">
<div>
<input id="official" type="checkbox" name="official" {{ if not .ProjectSettings.Personal }}checked{{ end }}>
<label for="official">Official HMN project</label>
</div>
<div>
<input id="hidden" type="checkbox" name="hidden" {{ if .ProjectSettings.Hidden }}checked{{ end }} />
<label for="hidden">Hide project</label>
</div>
<div>
<input id="featured" type="checkbox" name="featured" {{ if .ProjectSettings.Featured }}checked{{ end }} />
<label for="featured">Featured</label>
</div>
</div>
<div class="b mt3 mb2">Preview:</div>
<div id="desc-preview" class="w-100"></div>
</div>
</div>
<div class="edit-form-row">
<div></div>
<div>
{{ if .Editing }}
<input type="submit" value="Save" />
{{ else }}
<a class="button submit" href="javascript:;" onclick="gotoTab('assets');">Next</a>
{{ end }}
</div>
</div>
</div>
<div class="tab" data-name="Assets" data-slug="assets">
<div class="edit-form-row">
<div>Light theme logo:</div>
<div class="light_logo">
{{ template "image_selector.html" imageselectordata "light_logo" .ProjectSettings.LightLogo false }}
</div>
</div>
<div class="edit-form-row">
<div>Dark theme logo:</div>
<div class="dark_logo">
{{ template "image_selector.html" imageselectordata "dark_logo" .ProjectSettings.DarkLogo false }}
</div>
</div>
<div class="edit-form-row">
<div></div>
<div>
{{ if .Editing }}
<input type="submit" value="Save" />
{{ else }}
<input type="submit" value="Create project" />
{{ end }}
<div class="input-group">
<label for="slug">Slug</label>
<input type="text" id="slug" name="slug" maxlength="255" class="textbox" value="{{ .ProjectSettings.Slug }}">
<div class="f6">Has no effect for personal projects. Personal projects have a slug derived from the title.</div>
<div class="f6">If you change this, make sure to change DNS too!</div>
</div>
</div>
</fieldset>
{{ end }}
<div class="flex justify-end">
{{ if .Editing }}
<input class="btn-primary" type="submit" value="Save" />
{{ else }}
<input class="btn-primary" type="submit" value="Create Project" />
{{ end }}
</div>
</div>
</form>
<input type="file" multiple name="file_input" id="file_input" class="dn" />{{/* NOTE(mark): copied NOTE(asaf): Placing this outside the form to avoid submitting it to the server by accident */}}
<div class="flex-fair bl pa4 bg2">
<div id="desc-preview" class="w-100 post-content"></div>
</div>
</div>
<input type="file" multiple name="file_input" id="file_input" class="dn" />{{/* NOTE(mark): copied NOTE(asaf): Placing this outside the form to avoid submitting it to the server by accident */}}
<script>
let csrf = JSON.parse({{ csrftokenjs .Session }});
@ -248,8 +244,7 @@
const tag = document.querySelector('#tag');
const tagPreview = document.querySelector('#tag-preview');
function updateTagPreview() {
tagPreview.innerText = tag.value;
document.querySelector('#tag-discord-info').classList.toggle('dn', tag.value.length === 0);
tagPreview.innerText = tag.value || "[your tag]";
}
updateTagPreview();
tag.addEventListener('input', () => updateTagPreview());
@ -299,6 +294,7 @@
});
addOwnerButton.addEventListener("click", function(ev) {
ev.preventDefault();
startAddOwner();
});
@ -386,17 +382,31 @@
const logoMaxFileSize = {{ .LogoMaxFileSize }};
let lightLogoSelector = new ImageSelector(
const logoSelector = new ImageSelector(
document.querySelector("#project_form"),
logoMaxFileSize,
document.querySelector(".light_logo")
);
let darkLogoSelector = new ImageSelector(
function openLogoSelector(e) {
e.preventDefault();
logoSelector.openFileInput();
}
///////////////////////
// Header management //
///////////////////////
const headerMaxFileSize = {{ .HeaderMaxFileSize }};
const headerSelector = new ImageSelector(
document.querySelector("#project_form"),
logoMaxFileSize,
document.querySelector(".dark_logo")
headerMaxFileSize,
document.querySelector(".header_image")
);
function openHeaderSelector(e) {
e.preventDefault();
headerSelector.openFileInput();
}
//////////////////
// Asset upload //
@ -411,5 +421,121 @@
{{ .TextEditor.UploadUrl }}
);
///////////
// Links //
///////////
const linksContainer = document.querySelector("#links");
const linksJSONInput = document.querySelector("#links_json");
const linkTemplate = makeTemplateCloner("link_row");
const dummyLinkTemplate = makeTemplateCloner("link_row_dummy");
const initialLinks = JSON.parse("{{ .ProjectSettings.LinksJSON }}");
for (const link of initialLinks) {
const l = linkTemplate();
l.nameInput.value = link.name;
l.urlInput.value = link.url;
linksContainer.appendChild(l.root)
}
ensureLinksEmptyState();
projectForm.addEventListener("submit", () => updateLinksJSON());
function addLink(e) {
e.preventDefault();
linksContainer.appendChild(linkTemplate().root);
}
function deleteLink(e) {
e.preventDefault();
const l = e.target.closest(".link_row");
l.remove();
ensureLinksEmptyState();
}
function ensureLinksEmptyState() {
if (!linksContainer.querySelector(".link_row")) {
// Empty state is a single row
linksContainer.appendChild(linkTemplate().root);
}
}
function updateLinksJSON() {
const links = [];
for (const l of linksContainer.querySelectorAll(".link_row")) {
links.push({
"name": l.querySelector(".link_name").value,
"url": l.querySelector(".link_url").value,
});
}
linksJSONInput.value = JSON.stringify(links);
}
let draggingLink = null;
let linkDragStartY = 0;
let linkDragStartMouseY = 0;
function startLinkDrag(e) {
e.preventDefault();
const l = e.target.closest(".link_row");
const top = l.offsetTop;
l.insertAdjacentElement("beforebegin", dummyLinkTemplate().root);
document.querySelector("body").classList.add("grabbing");
l.style.position = "absolute";
l.style.top = `${top}px`;
l.classList.add("link_dragging");
draggingLink = l;
linkDragStartY = top;
linkDragStartMouseY = e.pageY;
}
function doLinkDrag(e) {
if (!draggingLink) {
return;
}
const maxTop = linksContainer.offsetHeight - draggingLink.offsetHeight;
const delta = e.pageY - linkDragStartMouseY;
const top = Math.max(0, Math.min(maxTop, linkDragStartY + delta));
const middle = top + draggingLink.offsetHeight/2;
draggingLink.style.top = `${top}px`;
const numLinks = linksContainer.querySelectorAll(".link_row").length;
const itemHeight = linksContainer.offsetHeight / numLinks;
const index = Math.floor(middle / itemHeight);
const links = linksContainer.querySelectorAll(".link_row:not(.link_dragging)");
const dummy = linksContainer.querySelector(".link_row_dummy");
dummy.remove();
linksContainer.insertBefore(dummy, links[index]);
}
function endLinkDrag(e) {
if (!draggingLink) {
return;
}
const dummy = linksContainer.querySelector(".link_row_dummy");
draggingLink.remove();
linksContainer.insertBefore(draggingLink, dummy);
dummy.remove();
draggingLink.style.position = null;
draggingLink.style.top = null;
draggingLink.classList.remove("link_dragging");
document.querySelector("body").classList.remove("grabbing");
draggingLink = null;
}
window.addEventListener("mouseup", endLinkDrag);
window.addEventListener("mousemove", doLinkDrag);
</script>
{{ end }}

View File

@ -9,8 +9,25 @@
{{ define "content" }}
<div class="flex flex-row justify-center">
<div class="flex-grow-1 flex flex-column items-center mw-site">
<div class="w-100 h5 bg-white-50"></div>
<div class="flex-grow-1 flex flex-column items-center mw-site pt4">
<div class="w-100 h5 bg-white-50 bg-center cover" style="background-image: url('{{ .Project.HeaderImage }}')">
<div class="flex justify-end pa3 g3 link--normal b">
{{ with .NamedLinks }}
<div class="bg--card-transparent flex">
{{ range . }}
<a class="flex ph3 pv2 flex items-center" href="{{ .Url }}">{{ .Name }}<span class="svgicon f6 ml2">{{ svg "arrow-right-up" }}</span></a>
{{ end }}
</div>
{{ end }}
{{ with .UnnamedLinks }}
<div class="bg--card-transparent flex items-center ph1">
{{ range . }}
<a class="flex ph2" href="{{ .Url }}" title="{{ .ServiceName }}{{ with .Username }} ({{ . }}){{ end }}">{{ svg (strjoin "logos/" .Icon) }}</a>
{{ end }}
</div>
{{ end }}
</div>
</div>
<div class="w-100 mw-site-narrow flex justify-center" style="margin-top: -3rem">
{{ template "project_card.html" projectcarddata .Project "flex-grow-1 project-card-black" }}
</div>
@ -56,10 +73,10 @@
<div class="flex-grow-1 overflow-hidden">
{{ with .Screenshots }}
<div class="carousel-container mw-100 mb3">
<div class="carousel aspect-ratio aspect-ratio--16x9 overflow-hidden bg--dim br2-ns">
<div class="carousel aspect-ratio aspect-ratio--16x9 overflow-hidden bg3 br2-ns">
<div class="dn db-l">
{{ range $index, $screenshot := . }}
<div class="carousel-item aspect-ratio--object bg--dim {{ if eq $index 0 }}active{{ end }}">
<div class="carousel-item aspect-ratio--object bg3 {{ if eq $index 0 }}active{{ end }}">
<div class="w-100 h-100" style="background:url('{{ $screenshot }}') no-repeat center / contain"></div>
</div>
{{ end }}

View File

@ -8,9 +8,9 @@
<div>
{{ with .OfficialProjects }}
<div class="carousel-container project-carousel mw-100 mv2 mv3-ns m--center dn db-ns">
<div class="carousel pa3 h5 overflow-hidden bg--dim br2-ns">
<div class="carousel pa3 h5 overflow-hidden bg3 br2-ns">
{{ range $index, $project := . }}
<div class="carousel-item flex pv3 pl3 w-100 h-100 bg--dim items-center {{ if eq $index 0 }}active{{ end }}">
<div class="carousel-item flex pv3 pl3 w-100 h-100 bg3 items-center {{ if eq $index 0 }}active{{ end }}">
<div class="flex-grow-1 pr3 relative flex flex-column h-100 justify-center">
<a href="{{ $project.Url }}">
<h3 class="f3">{{ $project.Name }}</h3>
@ -33,7 +33,7 @@
{{ end }}
<div class="flex flex-column g3">
{{ if .CurrentJamProjects }}
<div class="ph3 pt3 bg--dim br2 flex flex-column">
<div class="ph3 pt3 bg3 br2 flex flex-column">
<h2>{{ template "jam_name" .CurrentJamSlug }}</h2>
<p>These projects are submissions to the {{ template "jam_name" .CurrentJamSlug }}, which is happening <b>right now!</b> <a href="{{ .CurrentJamLink }}">Learn more »</a>
<div class="grid grid-1 grid-2-ns g3">
@ -45,7 +45,7 @@
</div>
{{ end }}
{{ if .OfficialProjects }}
<div class="ph3 pt3 bg--dim br2 flex flex-column">
<div class="ph3 pt3 bg3 br2 flex flex-column">
<h2 class="f3 mb2">Official Projects</h2>
<div class="grid grid-1 grid-2-ns g3">
{{ range .OfficialProjects }}
@ -56,7 +56,7 @@
</div>
{{ end }}
{{ if .PersonalProjects }}
<div class="ph3 pt3 bg--dim br2 flex flex-column">
<div class="ph3 pt3 bg3 br2 flex flex-column">
<h2 class="f3 mb2">Personal Projects</h2>
<div>Many community members have projects of their own. Want to join them? <a href="{{ .CreateProjectLink }}">Create your own.</a></div>
<div class="mt3 grid grid-1 grid-2-ns g3">
@ -68,7 +68,7 @@
</div>
{{ end }}
{{ if .PreviousJamProjects }}
<div class="ph3 pt3 bg--dim br2 flex flex-column">
<div class="ph3 pt3 bg3 br2 flex flex-column">
<h2>{{ template "jam_name" .PreviousJamSlug }}</h2>
<p>The following projects were submissions to our most recent jam. <a href="{{ .PreviousJamLink }}">Learn more »</a></p>
<div class="grid grid-1 grid-2-ns g3">
@ -100,7 +100,7 @@
<h2>{{ template "jam_name" .Category }}</h2>
<p>The following projects were submissions to the {{ template "jam_name" .Category }}. <a href="{{ .PageJamLink }}">Learn more »</a></p>
{{ end }}
<div class="bg--dim-ns br2">
<div class="bg3-ns br2">
{{ if gt .Pagination.Total 1 }}
<div class="optionbar pv2 ph3">
<div class="options"></div>

View File

@ -4,7 +4,7 @@
<title>Style test</title>
<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 rel="stylesheet" type="text/css" href="{{ static "style.build.css" }}" />
<link rel="stylesheet" type="text/css" href="{{ static "style.css" }}" />
</head>
<body class="ma3">
@ -80,28 +80,135 @@ int main() {
<div class="post-content pa3 ba b--theme-dim">
<h1>Heading 1</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<ol>
<li>List items without paragraphs.</li>
<li>
<p>List items with paragraphs.</p>
<p>These should also look reasonable.</p>
</li>
<li>And back to no paragraphs.</li>
</ol>
<h2>Heading 2</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Hello Handmade Network! We held our first-ever <a href="https://handmade.network/jam/learning-2024">Learning Jam</a> in March, in which participants learn about a topic and share that knowledge with the rest of the community. We had great turnout for an experimental jam in its first year, and Im excited to revisit it in the future.</p>
<p>At the end of the day, here's what's most important: <strong>projects,</strong> and the people who author them.</p>
<h2>Heading 2, which is a long heading that may wrap under certain circumstances, assuming the user's screen is narrow enough and the heading is long enough to horizontally overflow the user's display</h2>
<p>This year, <strong>we are 100% focused on projects.</strong> Our sole goal is to promote and boost the amazing work being done by the Handmade community. To that end:</p>
<ul>
<li>List items without paragraphs.</li>
<li>
<p>List items with paragraphs.</p>
<p>These should also look reasonable.</p>
</li>
<li>And back to no paragraphs.</li>
<li><strong>Were doing more jams.</strong> In addition to the Learning Jam, well bringing back both the Visibility Jam and Wheel Reinvention Jam for another year—and plan to keep doing so indefinitely.</li>
<li><strong>Were doubling down on Unwind.</strong> Our monthly Twitch show <a href="https://www.youtube.com/playlist?list=PL-IPpPzBYXBGsAd9-c2__x6LJG4Zszs0T">Unwind</a> is an opportunity to dig deeper into technical details with the authors of various projects. The first few episodes have been a great time, but theres so much more we can do with the show, and we hope to increase the shows reach so that even more people can be aware of the great work being done by members of the community.</li>
</ul>
<p>I'm really excited to see where we can take Handmade Network projects this year.</p>
<hr>
<p>Before I close, a few key project updates:</p>
<ul>
<li>
<p><strong><a href="https://voyager.handmade.network/">Disk Voyager</a></strong> is coming along beautifully and already has dozens of very happy alpha users. He recently added a bookmarks / quick access panel, which I am very excited about. It will soon enter open alpha, so go to <a href="https://diskvoyager.com/">https://diskvoyager.com/</a> and sign up to make sure you get access.</p>
<p>This is a second paragraph, just to see how it will look inside of a list item.</p>
</li>
<li>
<p><a href="https://cactus.handmade.network/"><strong>Cactus Image Viewer</strong></a> has been receiving lots of quality updates recently, with more on the way, including a gallery of other images in the folder. You can download the latest version from <a href="https://github.com/Wassimulator/CactusViewer">GitHub</a>.</p>
<p><img src="https://assets.media.handmade.network/8d3e2469-e380-4a10-a4bb-8c7e6c10ccdd/news-cactus.png" alt="A screenshot of Cactus Image Viewer's new gallery UI"></p>
</li>
<li>
<p><strong><a href="https://orca-app.dev/">Orca</a></strong> is on the cusp of another major release. <a href="https://handmade.network/m/bitwitch">Shaw</a> and I rewrote the Python tooling in C to reduce dependencies, <a href="https://handmade.network/m/rdunnington">Reuben</a> added a complete libc implementation (no more shim!), and Martin rewrote the <a href="https://orca-app.dev/posts/240426/vector_graphics.html">vector graphics backend</a> in WebGPU. Make sure to subscribe to the <a href="https://orca-app.dev/">Orca newsletter</a> to be notified when it releases.</p>
</li>
</ul>
<p>And finally, Abner has started a <a href="https://discord.gg/WRqC3AnA5B">Discord server</a> for Handmade Cities. You can read more about his rationale in <a href="https://handmadecities.com/new-discord-server/">this blog post</a>, but if you are interested in meetups or coworking with Handmade folks, I recommend you go join.</p>
<pre class="hmn-code"><span class="n">RECT</span> <span class="n">window_rect</span><span class="p">;</span>
<span class="n">GetWindowRect</span><span class="p">(</span> <span class="n">big_window</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">window_rect</span> <span class="p">);</span>
<span class="n">window_rect</span><span class="p">.</span><span class="n">top</span> <span class="o">+=</span> <span class="mi">30</span><span class="p">;</span>
</pre>
</div>
<h1 class="mt3 mb2">Form styles</h1>
<div class="hmn-form pa3 ba b--theme-dim flex flex-column g3">
<div class="flex flex-column g2">
<h2 class="">Inputs</h2>
<input placeholder="Test input">
<input class="error" value="Invalid value">
<input disabled value="Disabled input">
</div>
<div class="flex flex-column g2">
<h2>Textareas</h2>
<textarea placeholder="Test textarea"></textarea>
<textarea class="error">Invalid value</textarea>
<textarea disabled>Disabled textarea</textarea>
</div>
<div class="flex flex-column g2">
<h2>Selects</h2>
<select>
<option>Option 1</option>
<option>Option 2</option>
<option>Option 3</option>
</select>
<select class="error">
<option>Option 1</option>
<option>Option 2</option>
<option>Option 3</option>
</select>
<select disabled>
<option>Option 1</option>
<option>Option 2</option>
<option>Option 3</option>
</select>
</div>
<div class="flex flex-column g2">
<h2>Checkboxes</h2>
<div class="flex g2">
<input type="checkbox">
<input type="checkbox" checked>
</div>
</div>
<div class="flex flex-column g2">
<h2>Radio buttons</h2>
<div class="flex g2">
<input type="radio" name="radio-test">
<input type="radio" name="radio-test" checked>
</div>
</div>
<div class="flex flex-column g2">
<h2>Buttons</h2>
<div class="flex g2">
<button class="btn-primary">Click me</button>
<button>Click me</button>
<button disabled>Click me</button>
</div>
<div class="flex g2">
<input type="submit" value="Click me" class="btn-primary">
<input type="submit" value="Click me">
<input type="submit" value="Click me" disabled>
</div>
</div>
<div class="flex flex-column g2">
<h2>Input groups</h2>
<div class="input-group">
<label for="title">Project title</label>
<input id="title" name="title">
<div class="f6 tr">0/140</div>
</div>
<div class="input-group error">
<label for="title">Project title</label>
<input id="title" name="title" value="A name which is too long">
<div class="f6 tr">24/10</div>
<div class="f6 error-msg">The project title is too long.</div>
</div>
</div>
<div class="flex flex-column g2">
<h2>Fieldsets</h2>
<fieldset>
<legend>Full name</legend>
<div class="flex g3 pa3">
<input placeholder="First">
<input placeholder="Last">
</div>
</fieldset>
<fieldset>
<legend class="flex justify-between">
<span>Project Logo</span>
<a href="#" class="normal">+ Upload Project Logo</a>
</legend>
</fieldset>
</div>
</div>
<div class="h5"></div>
</body>

View File

@ -43,7 +43,7 @@
<div class="edit-form-row">
<div>Avatar:</div>
<div class="user_avatar">
{{ template "image_selector.html" imageselectordata "avatar" .User.AvatarUrl false }}
{{ template "image_selector.html" imageselectordata "avatar" .Avatar false }}
<div class="c--dim f7">(Allowed image types: PNG, JPEG and GIF. Avatars may weigh up to 1MB and will be resized if larger than 400x400 pixels)</div>
</div>
<script>

View File

@ -302,10 +302,10 @@ var HMNTemplateFuncs = template.FuncMap{
}
},
"imageselectordata": func(name string, src string, required bool) ImageSelectorData {
"imageselectordata": func(name string, src *Asset, required bool) ImageSelectorData {
return ImageSelectorData{
Name: name,
Src: src,
Asset: src,
Required: required,
}
},

View File

@ -131,7 +131,8 @@ type Project struct {
ParsedDescription template.HTML
Owners []User
Logo string
Logo string
HeaderImage string
LifecycleBadgeClass string
LifecycleString string
@ -157,11 +158,22 @@ type ProjectSettings struct {
Blurb string
Description string
LinksText string
LinksJSON string
Owners []User
LightLogo string
DarkLogo string
LightLogo *Asset
DarkLogo *Asset
HeaderImage *Asset
}
type Asset struct {
Url string
ID string
Filename string
Size int
MimeType string
Width, Height int
}
type ProjectJamParticipation struct {
@ -203,10 +215,11 @@ type User struct {
}
type Link struct {
Name string
Url string
LinkText string
Icon string
Name string `json:"name"`
Url string `json:"url"`
ServiceName string `json:"serviceName"`
Username string `json:"text"`
Icon string `json:"icon"`
}
type Podcast struct {
@ -358,7 +371,7 @@ type ProjectCardData struct {
type ImageSelectorData struct {
Name string
Src string
Asset *Asset
Required bool
}

View File

@ -83,7 +83,7 @@ func EpisodeList(c *RequestContext) ResponseData {
if t != foundTopic {
url = c.UrlContext.BuildEpisodeList(t)
}
topicLinks = append(topicLinks, templates.Link{LinkText: t, Url: url})
topicLinks = append(topicLinks, templates.Link{Username: t, Url: url})
}
var res ResponseData

View File

@ -1,35 +1,27 @@
package website
import (
"encoding/json"
"fmt"
"strings"
"git.handmade.network/hmn/hmn/src/models"
)
type ParsedLink struct {
Name string
Url string
Name string `json:"name"`
Url string `json:"url"`
}
func ParseLinks(text string) []ParsedLink {
lines := strings.Split(text, "\n")
res := make([]ParsedLink, 0, len(lines))
for _, line := range lines {
linkParts := strings.SplitN(line, " ", 2)
url := strings.TrimSpace(linkParts[0])
name := ""
if len(linkParts) > 1 {
name = strings.TrimSpace(linkParts[1])
}
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
continue
}
res = append(res, ParsedLink{Name: name, Url: url})
var links []ParsedLink
err := json.Unmarshal([]byte(text), &links)
if err != nil {
return nil
}
return res
return links
}
// TODO: Clean up use in user profiles I guess
func LinksToText(links []*models.Link) string {
linksText := ""
for _, link := range links {

View File

@ -2,6 +2,7 @@ package website
import (
"context"
"encoding/json"
"errors"
"fmt"
"image"
@ -318,17 +319,6 @@ func jamLink(jamSlug string) string {
}
}
type ProjectHomepageData struct {
templates.BaseData
Project templates.Project
Owners []templates.User
Screenshots []string
ProjectLinks []templates.Link
Licenses []templates.Link
RecentActivity []templates.TimelineItem
SnippetEdit templates.SnippetEdit
}
func ProjectHomepage(c *RequestContext) ResponseData {
maxRecentActivity := 15
@ -391,6 +381,16 @@ func ProjectHomepage(c *RequestContext) ResponseData {
})
c.Perf.EndBlock()
type ProjectHomepageData struct {
templates.BaseData
Project templates.Project
Owners []templates.User
Screenshots []string
NamedLinks, UnnamedLinks []templates.Link
RecentActivity []templates.TimelineItem
SnippetEdit templates.SnippetEdit
}
var templateData ProjectHomepageData
templateData.BaseData = getBaseData(c, c.CurrentProject.Name, nil)
@ -455,8 +455,12 @@ func ProjectHomepage(c *RequestContext) ResponseData {
templateData.Screenshots = append(templateData.Screenshots, hmnurl.BuildUserFile(screenshotFilename))
}
for _, link := range projectLinks {
templateData.ProjectLinks = append(templateData.ProjectLinks, templates.LinkToTemplate(link))
for _, link := range templates.LinksToTemplate(projectLinks) {
if link.Name != "" {
templateData.NamedLinks = append(templateData.NamedLinks, link)
} else {
templateData.UnnamedLinks = append(templateData.UnnamedLinks, link)
}
}
for _, post := range posts {
@ -528,6 +532,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
}
var ProjectLogoMaxFileSize = 2 * 1024 * 1024
var ProjectHeaderMaxFileSize = 2 * 1024 * 1024 // TODO(ben): Pick a real limit
type ProjectEditData struct {
templates.BaseData
@ -536,8 +541,8 @@ type ProjectEditData struct {
ProjectSettings templates.ProjectSettings
MaxOwners int
APICheckUsernameUrl string
LogoMaxFileSize int
APICheckUsernameUrl string
LogoMaxFileSize, HeaderMaxFileSize int
TextEditor templates.TextEditor
}
@ -581,6 +586,7 @@ func ProjectNew(c *RequestContext) ResponseData {
APICheckUsernameUrl: hmnurl.BuildAPICheckUsername(),
LogoMaxFileSize: ProjectLogoMaxFileSize,
HeaderMaxFileSize: ProjectHeaderMaxFileSize,
TextEditor: templates.TextEditor{
MaxFileSize: AssetMaxSize(c.CurrentUser),
@ -695,18 +701,15 @@ func ProjectEdit(c *RequestContext) ResponseData {
}
c.Perf.EndBlock()
lightLogoUrl := templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset, "light")
darkLogoUrl := templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset, "dark")
projectSettings := templates.ProjectToProjectSettings(
&p.Project,
p.Owners,
p.TagText(),
lightLogoUrl, darkLogoUrl,
p.LogoLightAsset, p.LogoDarkAsset, p.HeaderImage,
c.Theme,
)
projectSettings.LinksText = LinksToText(projectLinks)
projectSettings.LinksJSON = string(utils.Must1(json.Marshal(templates.LinksToTemplate(projectLinks))))
projectSettings.JamParticipation = make([]templates.ProjectJamParticipation, 0, len(projectJams))
for _, jam := range projectJams {
@ -726,6 +729,7 @@ func ProjectEdit(c *RequestContext) ResponseData {
APICheckUsernameUrl: hmnurl.BuildAPICheckUsername(),
LogoMaxFileSize: ProjectLogoMaxFileSize,
HeaderMaxFileSize: ProjectHeaderMaxFileSize,
TextEditor: templates.TextEditor{
MaxFileSize: AssetMaxSize(c.CurrentUser),
@ -784,6 +788,7 @@ type ProjectPayload struct {
OwnerUsernames []string
LightLogo FormImage
DarkLogo FormImage
HeaderImage FormImage
Tag string
JamParticipationSlugs []string
@ -850,6 +855,11 @@ func ParseProjectEditForm(c *RequestContext) ProjectEditFormResult {
res.Error = oops.New(err, "Failed to read image from form")
return res
}
headerImage, err := GetFormImage(c, "header_image")
if err != nil {
res.Error = oops.New(err, "Failed to read image from form")
return res
}
owners := c.Req.Form["owners"]
if len(owners) > maxProjectOwners {
@ -881,6 +891,7 @@ func ParseProjectEditForm(c *RequestContext) ProjectEditFormResult {
OwnerUsernames: owners,
LightLogo: lightLogo,
DarkLogo: darkLogo,
HeaderImage: headerImage,
Tag: tag,
JamParticipationSlugs: jamParticipationSlugs,
Slug: slug,
@ -926,6 +937,23 @@ func updateProject(ctx context.Context, tx pgx.Tx, user *models.User, payload *P
darkLogoUUID = &darkLogoAsset.ID
}
var headerImageUUID *uuid.UUID
if payload.HeaderImage.Exists {
headerImage := &payload.HeaderImage
headerImageAsset, err := assets.Create(ctx, tx, assets.CreateInput{
Content: headerImage.Content,
Filename: headerImage.Filename,
ContentType: headerImage.Mime,
UploaderID: &user.ID,
Width: headerImage.Width,
Height: headerImage.Height,
})
if err != nil {
return oops.New(err, "Failed to save asset")
}
headerImageUUID = &headerImageAsset.ID
}
hasSelf := false
selfUsername := strings.ToLower(user.Username)
for i, _ := range payload.OwnerUsernames {
@ -1021,6 +1049,23 @@ func updateProject(ctx context.Context, tx pgx.Tx, user *models.User, payload *P
}
}
if payload.HeaderImage.Exists || payload.HeaderImage.Remove {
_, err = tx.Exec(ctx,
`
UPDATE project
SET
header_asset_id = $2
WHERE
id = $1
`,
payload.ProjectID,
headerImageUUID,
)
if err != nil {
return oops.New(err, "Failed to update project's header image")
}
}
owners, err := db.Query[models.User](ctx, tx,
`
SELECT $columns

View File

@ -214,6 +214,7 @@ func UserSettings(c *RequestContext) ResponseData {
DefaultAvatarUrl string
User templates.User
Avatar *templates.Asset
Email string // these fields are handled specially on templates.User
ShowEmail bool
LinksText string
@ -290,6 +291,7 @@ func UserSettings(c *RequestContext) ResponseData {
AvatarMaxFileSize: UserAvatarMaxFileSize,
DefaultAvatarUrl: templates.UserAvatarDefaultUrl(c.Theme),
User: templateUser,
Avatar: templates.AssetToTemplate(c.CurrentUser.AvatarAsset),
Email: c.CurrentUser.Email,
ShowEmail: c.CurrentUser.ShowEmail,
LinksText: linksText,

View File

@ -1,15 +1,5 @@
# Styling TODO
## How to build
```
esbuild src\rawdata\scss\style.css --bundle --loader:.ttf=file --outdir=public --target=chrome109,firefox109,safari12
nodemon --exec "esbuild src\rawdata\scss\style.css --bundle --loader:.ttf=file --outdir=public --target=chrome109,firefox109,safari12" src\rawdata\scss\style.css --ignore public
```
## TODO
- [ ] Fix spacing of podcast episodes (used to use p-spaced)
- [x] Audit uses of tables across the site to see where we might actually need to apply table-layout: fixed and border-collapse: collapse
- There are zero tables on the site except one used for Asaf's debugging.
@ -47,3 +37,20 @@ nodemon --exec "esbuild src\rawdata\scss\style.css --bundle --loader:.ttf=file -
- [ ] Validate accessibility of navigation
- [ ] Make navigation work on mobile
- [ ] Clean up code TODOs
- [ ] Support the following external logos:
- Twitter / X
- Patreon
- Discord
- Twitch
- Steam
- Itch?
- Generic website
- Bluesky
- YouTube
- Vimeo
- App Store
- Play Store
- GitHub
- Threads?
- TikTok?
- Trello?