Rework the nav for projects 2.0

This commit is contained in:
Ben Visness 2021-10-20 21:21:24 -05:00
parent 307699af4c
commit ccdbad8978
12 changed files with 357 additions and 270 deletions

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
src/config/config.go
.vscode
.idea
vendor/
dbclones/
coverage.out
@ -11,4 +12,4 @@ annotations/
hmn.conf
adminmailer/config.go
adminmailer/adminmailer
local/backups
local/backups

View File

@ -1377,7 +1377,7 @@ img, video {
.bw0 {
border-width: 0; }
.bw1 {
.bw1, header .submenu {
border-width: 0.125rem; }
.bw2 {
@ -4581,7 +4581,7 @@ code, .code {
.pa1 {
padding: 0.25rem; }
.pa2, header .menu-bar .items a, .tab {
.pa2, .tab, header .root-item > a, header .submenu > a {
padding: 0.5rem; }
.pa3, header #login-popup {
@ -4707,7 +4707,7 @@ code, .code {
padding-top: 0.25rem;
padding-bottom: 0.25rem; }
.pv2, header .menu-bar .items a.project-logo, .tab-bar .tab-button,
.pv2, .tab-bar .tab-button,
button,
.button,
input[type=button],
@ -5053,7 +5053,7 @@ input[type=submit], .notice {
.optionbar .options input[type=submit] {
padding-top: 0.5rem;
padding-bottom: 0.5rem; }
.pv3-ns, header .menu-bar .items a {
.pv3-ns {
padding-top: 1rem;
padding-bottom: 1rem; }
.pv4-ns {
@ -5112,7 +5112,7 @@ input[type=submit], .notice {
margin-left: 0; }
.ml1-ns {
margin-left: 0.25rem; }
.ml2-ns, header .menu-bar .items a:first-child {
.ml2-ns {
margin-left: 0.5rem; }
.ml3-ns {
margin-left: 1rem; }
@ -5593,7 +5593,7 @@ input[type=submit], .notice {
.ph2-l {
padding-left: 0.5rem;
padding-right: 0.5rem; }
.ph3-l, header .menu-bar .items a {
.ph3-l, header .root-item > a, header .submenu > a {
padding-left: 1rem;
padding-right: 1rem; }
.ph4-l {
@ -5630,7 +5630,7 @@ input[type=submit], .notice {
margin-left: 0.25rem; }
.ml2-l {
margin-left: 0.5rem; }
.ml3-l, header .menu-bar .items a:first-child {
.ml3-l {
margin-left: 1rem; }
.ml4-l {
margin-left: 2rem; }
@ -7381,7 +7381,7 @@ article code {
color: #ccc;
color: var(--theme-color-dimmest); }
.b--dimmest, header #login-popup, .optionbar, blockquote, .post-content th, .post-content td {
.b--dimmest, .optionbar, blockquote, .post-content th, .post-content td, header #login-popup {
border-color: #bbb;
border-color: var(--dimmest-color); }
@ -7578,6 +7578,7 @@ article code {
.svgicon svg {
fill: currentColor;
stroke: currentColor;
width: 1em;
height: 1em; }
@ -7588,64 +7589,6 @@ article code {
margin-right: auto;
margin-left: auto; }
header .hmn-logo {
height: 3.75rem;
width: 100%;
text-transform: uppercase;
font-family: 'MohaveHMN', sans-serif;
font-size: 2rem;
display: flex;
align-items: center;
justify-content: center;
color: white !important; }
@media screen and (min-width: 30em) {
header .hmn-logo {
width: 11.25rem; } }
header .menu-bar {
width: 100%;
z-index: 10; }
header .menu-bar .items a {
font-weight: bold; }
header .menu-bar .items a.patreon {
float: right;
height: 30px;
padding-top: 18px;
display: inline-block; }
header .menu-bar .items a h1 {
display: inline; }
header .user-options {
position: relative; }
header .login, header .register {
text-align: center; }
header #login-popup {
background-color: #fbfbfb;
background-color: var(--login-popup-background);
color: black;
color: var(--fg-font-color);
border-width: 1px;
border-style: dashed;
visibility: hidden;
position: absolute;
z-index: 12;
margin-top: 10px;
right: 0px;
top: 20px;
width: 290px;
max-height: 0px;
overflow: hidden;
opacity: 0;
transition: all 0.2s; }
header #login-popup.open {
max-height: 170px;
opacity: 1;
visibility: visible; }
header #login-popup label {
padding-right: 10px; }
@media screen and (min-width: 30em) {
footer .list li:not(:last-child)::after {
content: ' / '; } }
@ -8762,6 +8705,102 @@ div.mark_as_read_toplevel_blog {
.bbtable tbody tr:nth-child(even) {
background: rgba(0, 0, 0, 0.05); }
header .hmn-logo {
height: 3.75rem;
width: 100%;
text-transform: uppercase;
font-family: 'MohaveHMN', sans-serif;
font-size: 2rem;
display: flex;
align-items: center;
justify-content: center;
color: white !important; }
@media screen and (min-width: 30em) {
header .hmn-logo {
width: 11.25rem; } }
header .items {
position: relative; }
@media screen and (min-width: 30em) {
header .root-item {
position: relative;
height: 3.75rem; } }
header .root-item:not(:hover):not(.clicked) > .submenu {
display: none; }
header .root-item.clicked .svgicon {
transform: rotate(180deg); }
header .root-item > a {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-weight: bold; }
header .root-item .svgicon {
font-size: 0.7em; }
header .submenu {
background-color: #f8f8f8;
background-color: var(--content-background);
display: flex;
flex-direction: column;
position: absolute;
left: 0;
right: 0;
z-index: 1;
min-width: 10rem;
border-top-style: solid;
border-bottom-style: solid; }
@media screen and (min-width: 30em) {
header .submenu {
border-top-style: none;
border-left-style: solid;
border-right-style: solid;
left: initial;
right: initial; } }
header .submenu > a {
display: block;
white-space: nowrap;
z-index: 1;
font-weight: bold;
text-align: center; }
@media screen and (min-width: 30em) {
header .submenu > a {
text-align: left; } }
header .menu-bar {
width: 100%;
z-index: 10; }
header #login-popup {
background-color: #fbfbfb;
background-color: var(--login-popup-background);
color: black;
color: var(--fg-font-color);
border-width: 1px;
border-style: dashed;
visibility: hidden;
position: absolute;
z-index: 12;
margin-top: 10px;
right: 0px;
top: 20px;
width: 290px;
max-height: 0px;
overflow: hidden;
opacity: 0;
transition: all 0.2s; }
header #login-popup.open {
max-height: 170px;
opacity: 1;
visibility: visible; }
header #login-popup label {
padding-right: 10px; }
@font-face {
font-family: icons;
src: url("/public/icons.ttf?v=4"); }

View File

@ -408,6 +408,7 @@ article code {
.svgicon {
svg {
fill: currentColor;
stroke: currentColor;
width: 1em;
height: 1em;
}
@ -424,100 +425,6 @@ article code {
margin-left: auto;
}
header {
.hmn-logo {
height: px2rem(60px);
width: 100%;
text-transform: uppercase;
font-family: 'MohaveHMN', sans-serif;
font-size: 2rem;
display: flex;
align-items: center;
justify-content: center;
color: white !important;
@media #{$breakpoint-not-small} {
width: px2rem(180px);
}
}
.menu-bar {
width: 100%;
z-index: 10;
.items a {
@extend .pa2;
@extend .pv3-ns;
@extend .ph3-l;
font-weight: bold;
&:first-child {
@extend .ml2-ns;
@extend .ml3-l;
}
&.patreon {
float:right;
height:30px;
padding-top:18px;
display:inline-block;
}
&.project-logo {
@extend .pv2;
}
h1 {
display: inline;
}
}
}
.user-options {
position: relative;
}
.login, .register {
text-align:center;
}
#login-popup {
@include usevar(background-color, login-popup-background);
@include usevar(color, fg-font-color);
@extend .pa3;
border-width: 1px;
border-style: dashed;
@extend .b--dimmest;
visibility: hidden;
position: absolute;
z-index: 12;
margin-top: 10px;
right: 0px;
top: 20px;
width: 290px;
max-height: 0px;
overflow: hidden;
opacity: 0;
transition: all 0.2s;
&.open {
max-height: 170px;
opacity: 1;
visibility: visible;
}
label {
padding-right:10px;
}
}
}
footer {
.list li:not(:last-child)::after {
@extend .c--dimmer;

View File

@ -0,0 +1,135 @@
header {
$logo-height: px2rem(60px);
.hmn-logo {
height: $logo-height;
width: 100%;
text-transform: uppercase;
font-family: 'MohaveHMN', sans-serif;
font-size: 2rem;
display: flex;
align-items: center;
justify-content: center;
color: white !important;
@media #{$breakpoint-not-small} {
width: px2rem(180px);
}
}
.items {
position: relative; // will be used on mobile, when .root-item is not relative
}
.root-item {
@media #{$breakpoint-not-small} {
& {
position: relative; // makes submenus align to this item instead of the screen
height: $logo-height;
}
}
&:not(:hover):not(.clicked) > .submenu {
display: none;
}
&.clicked .svgicon {
transform: rotate(180deg);
}
> a {
@extend .pa2, .ph3-l;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-weight: bold;
}
.svgicon {
font-size: 0.7em;
}
}
.submenu {
@extend .bw1;
@include usevar(background-color, content-background);
display: flex;
flex-direction: column;
position: absolute;
left: 0;
right: 0;
z-index: 1;
min-width: 10rem;
border-top-style: solid;
border-bottom-style: solid;
@media #{$breakpoint-not-small} {
& {
border-top-style: none;
border-left-style: solid;
border-right-style: solid;
left: initial;
right: initial;
}
}
> a {
@extend .pa2, .ph3-l;
display: block;
white-space: nowrap;
z-index: 1;
font-weight: bold;
text-align: center;
@media #{$breakpoint-not-small} {
& {
text-align: left;
}
}
}
}
.menu-bar {
width: 100%;
z-index: 10;
}
#login-popup {
@include usevar(background-color, login-popup-background);
@include usevar(color, fg-font-color);
@extend .pa3;
border-width: 1px;
border-style: dashed;
@extend .b--dimmest;
visibility: hidden;
position: absolute;
z-index: 12;
margin-top: 10px;
right: 0px;
top: 20px;
width: 290px;
max-height: 0px;
overflow: hidden;
opacity: 0;
transition: all 0.2s;
&.open {
max-height: 170px;
opacity: 1;
visibility: visible;
}
label {
padding-right:10px;
}
}
}

View File

@ -14,6 +14,7 @@
@import 'episodes';
@import 'forms';
@import 'forum';
@import 'header';
@import 'icons';
@import 'irc';
@import 'landing';

View File

@ -1,4 +1,4 @@
<header class="mb3 bb bw1 b--theme-dark">
<header id="site-header" class="mb3 bb bw1 b--theme-dark">
<div class="user-options flex justify-center justify-end-ns">
{{ if .User }}
{{ if .User.IsStaff }}
@ -30,59 +30,81 @@
<a href="{{ .Header.HMNHomepageUrl }}" class="hmn-logo bg-theme-dark">
Handmade
</a>
<div class="items flex items-center justify-center justify-start-ns">
{{ if .IsProjectPage }}
<a class="project-logo" href="{{ .Header.ProjectHomepageUrl }}">
<h1>{{ .Project.Name }}</h1>
</a>
{{ end }}
{{ if not .IsProjectPage }}
<a href="{{ .Header.ProjectIndexUrl }}" class="projects">Projects</a>
{{ end }}
{{ if .Project.HasBlog }}
<a href="{{ .Header.BlogUrl }}" class="blog">{{ if .IsProjectPage }}Blog{{ else }}News{{ end }}</a>
{{ end }}
{{ if .Project.HasForum }}
<a href="{{ .Header.ForumsUrl }}" class="forums">Forums</a>
{{ end }}
{{ if .Project.HasLibrary }}
<a href="{{ .Header.LibraryUrl }}" class="library">Library</a>
{{ end }}
{{ if .Project.IsHMN }}
<a href="{{ .Header.ManifestoUrl }}" class="misson">Mission</a>
{{ end }}
{{ if .Header.EpisodeGuideUrl }}
<a href="{{ .Header.EpisodeGuideUrl }}" class="annotations">Episode Guide</a>
{{ end }}
{{ if .Header.EditUrl }}
<a class="edit" href="{{ .Header.EditUrl }}" title="Edit {{ .Project.Name }}"><span class="icon">0</span>&nbsp;Settings</a>
{{ end }}
<div class="items flex items-center justify-center justify-start-ns ml2-ns ml3-l">
<div class="root-item">
<a href="{{ .Header.ProjectIndexUrl }}">Projects</a>
</div>
<div class="root-item">
<a>Media <div class="dib svgicon ml1">{{ svg "chevron-down-thick" }}</div></a>
<div class="submenu b--theme-dark">
<a href="{{ .Header.PodcastUrl }}">Podcast</a>
<a href="https://handmadedev.show/" target="_blank">Handmade Dev Show</a>
</div>
</div>
<div class="root-item">
<a href="{{ .Header.ForumsUrl }}">Forums</a>
</div>
<div class="root-item">
<a>Resources <div class="dib svgicon ml1">{{ svg "chevron-down-thick" }}</div></a>
<div class="submenu b--theme-dark">
<a href="{{ .Header.LibraryUrl }}">Library</a>
</div>
</div>
</div>
</div>
<form onsubmit="this.querySelector('input[name=q]').value = this.querySelector('#searchstring').value + ' site:handmade.network';" class="dn ma0 flex-l flex-column justify-center items-end" method="GET" action="{{ .Header.SearchActionUrl }}" target="_blank">
<input type="hidden" name="q" />
<input class="site-search bn lite pa2 fira" type="text" id="searchstring" value="" placeholder="Search with DuckDuckGo" size="18" />
<input id="search_button_homepage" type="submit" value="Go"/>
</form>
</div>
</header>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
var loginPopup = document.getElementById("login-popup");
var loginLink = document.getElementById("login-link");
const header = document.querySelector('#site-header');
if (loginPopup !== null) {
loginLink.removeAttribute("href");
// set up dropdown stuff for mobile / touch
{
const rootItems = header.querySelectorAll('.root-item');
loginLink.onclick = function() {
loginPopup.classList.toggle("open");
function clearDropdowns() {
for (const item of rootItems) {
item.classList.remove('clicked');
}
}
function clickDropdown(el) {
if (el.classList.contains('clicked')) {
clearDropdowns();
} else {
clearDropdowns();
el.classList.add('clicked');
}
}
for (const item of rootItems) {
if (item.querySelector('.submenu')) {
item.addEventListener('click', e => {
clickDropdown(item);
e.stopPropagation();
});
}
}
}
for (const time of document.querySelectorAll('time')) {
const d = new Date(Date.parse(time.dateTime));
time.title = d.toLocaleString();
// set up login form
{
const loginPopup = document.getElementById("login-popup");
const loginLink = document.getElementById("login-link");
if (loginPopup !== null) {
loginLink.removeAttribute("href");
loginLink.onclick = () => {
loginPopup.classList.toggle("open");
}
}
for (const time of document.querySelectorAll('time')) {
const d = new Date(Date.parse(time.dateTime));
time.title = d.toLocaleString();
}
}
});
</script>

View File

@ -0,0 +1,5 @@
<?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 186 186" 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-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<path d="M16,57L93,129L170,57" style="fill:none;stroke-width:32px;"/>
</svg>

After

Width:  |  Height:  |  Size: 549 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 186 186" 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(6.12323e-17,1,-1,6.12323e-17,185.342,0.00025)">
<path d="M51.707,185.343C48.966,185.343 46.214,184.299 44.114,182.194C39.92,178 39.92,171.213 44.114,167.019L118.466,92.672L44.114,18.32C39.92,14.126 39.92,7.333 44.114,3.145C48.308,-1.049 55.101,-1.049 59.294,3.145L141.228,85.079C145.422,89.273 145.422,96.066 141.228,100.254L59.294,182.193C57.201,184.293 54.454,185.343 51.707,185.343Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 910 B

View File

@ -71,32 +71,8 @@ func names(ts []*template.Template) []string {
return result
}
//go:embed svg/close.svg
var SVGClose string
//go:embed svg/chevron-left.svg
var SVGChevronLeft string
//go:embed svg/chevron-right.svg
var SVGChevronRight string
//go:embed svg/appleinc.svg
var SVGAppleInc string
//go:embed svg/google.svg
var SVGGoogle string
//go:embed svg/spotify.svg
var SVGSpotify string
var SVGMap = map[string]string{
"close": SVGClose,
"chevron-left": SVGChevronLeft,
"chevron-right": SVGChevronRight,
"appleinc": SVGAppleInc,
"google": SVGGoogle,
"spotify": SVGSpotify,
}
//go:embed svg/*
var SVGs embed.FS
var HMNTemplateFuncs = template.FuncMap{
"add": func(a int, b ...int) int {
@ -185,8 +161,8 @@ var HMNTemplateFuncs = template.FuncMap{
}
},
"svg": func(name string) template.HTML {
contents, found := SVGMap[name]
if !found {
contents, err := SVGs.ReadFile(fmt.Sprintf("svg/%s.svg", name))
if err != nil {
panic("SVG not found: " + name)
}
return template.HTML(contents)

View File

@ -37,23 +37,19 @@ func (bd *BaseData) AddImmediateNotice(class, content string) {
}
type Header struct {
AdminUrl string
UserProfileUrl string
UserSettingsUrl string
LoginActionUrl string
LogoutActionUrl string
ForgotPasswordUrl string
RegisterUrl string
HMNHomepageUrl string
ProjectHomepageUrl string
ProjectIndexUrl string
BlogUrl string
ForumsUrl string
LibraryUrl string
ManifestoUrl string
EpisodeGuideUrl string
EditUrl string
SearchActionUrl string
AdminUrl string
UserProfileUrl string
UserSettingsUrl string
LoginActionUrl string
LogoutActionUrl string
ForgotPasswordUrl string
RegisterUrl string
HMNHomepageUrl string
ProjectIndexUrl string
PodcastUrl string
ForumsUrl string
LibraryUrl string
}
type Footer struct {

View File

@ -364,7 +364,8 @@ func ProjectHomepage(c *RequestContext) ResponseData {
projectHomepageData.BaseData = getBaseData(c, project.Name, nil)
if canEdit {
projectHomepageData.BaseData.Header.EditUrl = hmnurl.BuildProjectEdit(project.Slug, "")
// TODO: Move to project-specific navigation
// projectHomepageData.BaseData.Header.EditURL = hmnurl.BuildProjectEdit(project.Slug, "")
}
projectHomepageData.BaseData.OpenGraphItems = append(projectHomepageData.BaseData.OpenGraphItems, templates.OpenGraphItem{
Property: "og:description",

View File

@ -304,11 +304,12 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
}
}
episodeGuideUrl := ""
defaultTopic, hasAnnotations := config.Config.EpisodeGuide.Projects[c.CurrentProject.Slug]
if hasAnnotations {
episodeGuideUrl = hmnurl.BuildEpisodeList(c.CurrentProject.Slug, defaultTopic)
}
// TODO: move to project-specific navigation
// episodeGuideUrl := ""
// defaultTopic, hasAnnotations := config.Config.EpisodeGuide.Projects[c.CurrentProject.Slug]
// if hasAnnotations {
// episodeGuideUrl = hmnurl.BuildEpisodeList(c.CurrentProject.Slug, defaultTopic)
// }
baseData := templates.BaseData{
Theme: c.Theme,
@ -330,22 +331,18 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
IsProjectPage: !c.CurrentProject.IsHMN(),
Header: templates.Header{
AdminUrl: hmnurl.BuildAdminApprovalQueue(), // TODO(asaf): Replace with general-purpose admin page
UserSettingsUrl: hmnurl.BuildUserSettings(""),
LoginActionUrl: hmnurl.BuildLoginAction(c.FullUrl()),
LogoutActionUrl: hmnurl.BuildLogoutAction(c.FullUrl()),
ForgotPasswordUrl: hmnurl.BuildRequestPasswordReset(),
RegisterUrl: hmnurl.BuildRegister(),
HMNHomepageUrl: hmnurl.BuildHomepage(),
ProjectHomepageUrl: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
ProjectIndexUrl: hmnurl.BuildProjectIndex(1),
BlogUrl: hmnurl.BuildBlog(c.CurrentProject.Slug, 1),
ForumsUrl: hmnurl.BuildForum(c.CurrentProject.Slug, nil, 1),
LibraryUrl: hmnurl.BuildLibrary(c.CurrentProject.Slug),
ManifestoUrl: hmnurl.BuildManifesto(),
EpisodeGuideUrl: episodeGuideUrl,
EditUrl: "",
SearchActionUrl: "https://duckduckgo.com",
AdminUrl: hmnurl.BuildAdminApprovalQueue(), // TODO(asaf): Replace with general-purpose admin page
UserSettingsUrl: hmnurl.BuildUserSettings(""),
LoginActionUrl: hmnurl.BuildLoginAction(c.FullUrl()),
LogoutActionUrl: hmnurl.BuildLogoutAction(c.FullUrl()),
ForgotPasswordUrl: hmnurl.BuildRequestPasswordReset(),
RegisterUrl: hmnurl.BuildRegister(),
HMNHomepageUrl: hmnurl.BuildHomepage(),
ProjectIndexUrl: hmnurl.BuildProjectIndex(1),
PodcastUrl: hmnurl.BuildPodcast(c.CurrentProject.Slug),
ForumsUrl: hmnurl.BuildForum(c.CurrentProject.Slug, nil, 1),
LibraryUrl: hmnurl.BuildLibrary(c.CurrentProject.Slug),
},
Footer: templates.Footer{
HomepageUrl: hmnurl.BuildHomepage(),