Add interactive link previews
This commit is contained in:
parent
bab955aaff
commit
51ad8d03d4
Binary file not shown.
|
@ -8484,6 +8484,23 @@ span.icon-rss::before {
|
||||||
height: var(--image-size);
|
height: var(--image-size);
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
.project-header-img {
|
||||||
|
width: 100%;
|
||||||
|
height: var(--height-5);
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
.project-links {
|
||||||
|
background-color: var(--c-transparent-background);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
--link-color: var(--color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.project-links::after {
|
||||||
|
content: "\200b";
|
||||||
|
padding: var(--spacing-2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* src/rawdata/scss/showcase.css */
|
/* src/rawdata/scss/showcase.css */
|
||||||
.showcase-item .gradient {
|
.showcase-item .gradient {
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
package links
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
//
|
||||||
|
// This is all in its own package so we can compile it to wasm without building extra junk.
|
||||||
|
//
|
||||||
|
|
||||||
|
// An online site/service for which we recognize the link
|
||||||
|
type Service struct {
|
||||||
|
Name string
|
||||||
|
IconName string
|
||||||
|
Regex *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
var Services = []Service{
|
||||||
|
// {
|
||||||
|
// Name: "itch.io",
|
||||||
|
// IconName: "itch",
|
||||||
|
// Regex: regexp.MustCompile(`://(?P<username>[\w-]+)\.itch\.io`),
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
Name: "App Store",
|
||||||
|
IconName: "app-store",
|
||||||
|
Regex: regexp.MustCompile(`^https?://apps.apple.com`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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(`^https?://github\.com/(?P<username>[\w/-]+)`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "GitLab",
|
||||||
|
IconName: "gitlab",
|
||||||
|
Regex: regexp.MustCompile(`^https?://gitlab\.com/(?P<username>[\w/-]+)`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Google Play",
|
||||||
|
IconName: "google-play",
|
||||||
|
Regex: regexp.MustCompile(`^https?://play\.google\.com`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Patreon",
|
||||||
|
IconName: "patreon",
|
||||||
|
Regex: regexp.MustCompile(`^https?://patreon\.com/(?P<username>[\w-]+)`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Twitch",
|
||||||
|
IconName: "twitch",
|
||||||
|
Regex: regexp.MustCompile(`^https?://twitch\.tv/(?P<username>[\w/-]+)`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 ParseKnownServicesForUrl(url string) (service Service, username string) {
|
||||||
|
for _, svc := range Services {
|
||||||
|
match := svc.Regex.FindStringSubmatch(url)
|
||||||
|
if match != nil {
|
||||||
|
username := ""
|
||||||
|
if idx := svc.Regex.SubexpIndex("username"); idx >= 0 {
|
||||||
|
username = match[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc, username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Service{
|
||||||
|
IconName: "website",
|
||||||
|
}, ""
|
||||||
|
}
|
|
@ -5,16 +5,25 @@ package main
|
||||||
import (
|
import (
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
|
|
||||||
|
"git.handmade.network/hmn/hmn/src/links"
|
||||||
"git.handmade.network/hmn/hmn/src/parsing"
|
"git.handmade.network/hmn/hmn/src/parsing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
js.Global().Set("parseMarkdown", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
js.Global().Set("parseMarkdown", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
return parsing.ParseMarkdown(args[0].String(), parsing.ForumPreviewMarkdown)
|
return parsing.ParseMarkdown(args[0].String(), parsing.ForumPreviewMarkdown)
|
||||||
}))
|
}))
|
||||||
js.Global().Set("parseMarkdownEdu", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
js.Global().Set("parseMarkdownEdu", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
return parsing.ParseMarkdown(args[0].String(), parsing.EducationPreviewMarkdown)
|
return parsing.ParseMarkdown(args[0].String(), parsing.EducationPreviewMarkdown)
|
||||||
}))
|
}))
|
||||||
|
js.Global().Set("parseKnownServicesForUrl", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
|
service, username := links.ParseKnownServicesForUrl(args[0].String())
|
||||||
|
return js.ValueOf(map[string]any{
|
||||||
|
"service": service.Name,
|
||||||
|
"icon": service.IconName,
|
||||||
|
"username": username,
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
var done chan struct{}
|
var done chan struct{}
|
||||||
<-done // block forever
|
<-done // block forever
|
||||||
|
|
|
@ -72,3 +72,26 @@
|
||||||
height: var(--image-size);
|
height: var(--image-size);
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-header-img {
|
||||||
|
/* w-100 h5 bg-white-50 bg-center cover */
|
||||||
|
width: 100%;
|
||||||
|
height: var(--height-5);
|
||||||
|
/* TODO(redesign): Better placeholder */
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-links {
|
||||||
|
background-color: var(--c-transparent-background);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
--link-color: var(--color);
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
/* make sure secondary links render at the right height despite SVG size */
|
||||||
|
&::after {
|
||||||
|
content: '\200b';
|
||||||
|
padding: var(--spacing-2) 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,13 +4,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/calendar"
|
"git.handmade.network/hmn/hmn/src/calendar"
|
||||||
"git.handmade.network/hmn/hmn/src/hmndata"
|
"git.handmade.network/hmn/hmn/src/hmndata"
|
||||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||||
|
"git.handmade.network/hmn/hmn/src/links"
|
||||||
"git.handmade.network/hmn/hmn/src/models"
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -240,95 +240,8 @@ var UnknownUser = User{
|
||||||
AvatarUrl: UserAvatarUrl(nil),
|
AvatarUrl: UserAvatarUrl(nil),
|
||||||
}
|
}
|
||||||
|
|
||||||
// An online site/service for which we recognize the link
|
|
||||||
type LinkService struct {
|
|
||||||
Name string
|
|
||||||
IconName string
|
|
||||||
Regex *regexp.Regexp
|
|
||||||
}
|
|
||||||
|
|
||||||
var LinkServices = []LinkService{
|
|
||||||
// {
|
|
||||||
// Name: "itch.io",
|
|
||||||
// IconName: "itch",
|
|
||||||
// Regex: regexp.MustCompile(`://(?P<username>[\w-]+)\.itch\.io`),
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
Name: "App Store",
|
|
||||||
IconName: "app-store",
|
|
||||||
Regex: regexp.MustCompile(`^https?://apps.apple.com`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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(`^https?://github\.com/(?P<username>[\w/-]+)`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "GitLab",
|
|
||||||
IconName: "gitlab",
|
|
||||||
Regex: regexp.MustCompile(`^https?://gitlab\.com/(?P<username>[\w/-]+)`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Google Play",
|
|
||||||
IconName: "google-play",
|
|
||||||
Regex: regexp.MustCompile(`^https?://play\.google\.com`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Patreon",
|
|
||||||
IconName: "patreon",
|
|
||||||
Regex: regexp.MustCompile(`^https?://patreon\.com/(?P<username>[\w-]+)`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Twitch",
|
|
||||||
IconName: "twitch",
|
|
||||||
Regex: regexp.MustCompile(`^https?://twitch\.tv/(?P<username>[\w/-]+)`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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, username string) {
|
|
||||||
for _, svc := range LinkServices {
|
|
||||||
match := svc.Regex.FindStringSubmatch(link.URL)
|
|
||||||
if match != nil {
|
|
||||||
username := ""
|
|
||||||
if idx := svc.Regex.SubexpIndex("username"); idx >= 0 {
|
|
||||||
username = match[idx]
|
|
||||||
}
|
|
||||||
|
|
||||||
return svc, username
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return LinkService{
|
|
||||||
IconName: "website",
|
|
||||||
}, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func LinkToTemplate(link *models.Link) Link {
|
func LinkToTemplate(link *models.Link) Link {
|
||||||
service, username := ParseKnownServicesForLink(link)
|
service, username := links.ParseKnownServicesForUrl(link.URL)
|
||||||
return Link{
|
return Link{
|
||||||
Name: link.Name,
|
Name: link.Name,
|
||||||
Url: link.URL,
|
Url: link.URL,
|
||||||
|
|
|
@ -5,14 +5,14 @@
|
||||||
</legend>
|
</legend>
|
||||||
<div class="pa3 input-group">
|
<div class="pa3 input-group">
|
||||||
<div id="links" class="flex flex-column g2 relative">
|
<div id="links" class="flex flex-column g2 relative">
|
||||||
<div class="b">Primary Links</div>
|
<div class="b primary_links">Primary Links</div>
|
||||||
<div class="b drop_slot secondary_links">Secondary Links</div>
|
<div class="b drop_slot secondary_links">Secondary Links</div>
|
||||||
</div>
|
</div>
|
||||||
<template id="link_row">
|
<template id="link_row">
|
||||||
<div class="link_row drop_slot w-100 flex flex-row items-center" data-tmpl="root">
|
<div class="link_row drop_slot w-100 flex flex-row items-center" data-tmpl="root">
|
||||||
<span class="link_handle svgicon pr3 pointer grab" onmousedown="startLinkDrag(event)">{{ svg "draggable" }}</span>
|
<span class="link_handle svgicon pr3 pointer grab" onmousedown="startLinkDrag(event)">{{ svg "draggable" }}</span>
|
||||||
<input data-tmpl="nameInput" class="link_name mr3 w5" type="text" placeholder="Name" />
|
<input data-tmpl="nameInput" class="link_name mr3 w5" type="text" placeholder="Name" oninput="linkInput(event)" />
|
||||||
<input data-tmpl="urlInput" class="link_url flex-grow-1" type="url" placeholder="Link" />
|
<input data-tmpl="urlInput" class="link_url flex-grow-1" type="url" placeholder="Link" oninput="linkInput(event)" />
|
||||||
<a class="delete_link svgicon link-normal pl3 f3" href="javascript:;" onclick="deleteLink(event)">{{ svg "delete" }}</a>
|
<a class="delete_link svgicon link-normal pl3 f3" href="javascript:;" onclick="deleteLink(event)">{{ svg "delete" }}</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -51,6 +51,8 @@
|
||||||
function addLink(e) {
|
function addLink(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
linksContainer.appendChild(linkTemplate().root);
|
linksContainer.appendChild(linkTemplate().root);
|
||||||
|
|
||||||
|
fireLinkEditEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteLink(e) {
|
function deleteLink(e) {
|
||||||
|
@ -59,6 +61,8 @@
|
||||||
l.remove();
|
l.remove();
|
||||||
|
|
||||||
ensureLinksEmptyState();
|
ensureLinksEmptyState();
|
||||||
|
|
||||||
|
fireLinkEditEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureLinksEmptyState() {
|
function ensureLinksEmptyState() {
|
||||||
|
@ -169,9 +173,19 @@
|
||||||
|
|
||||||
document.body.classList.remove("grabbing");
|
document.body.classList.remove("grabbing");
|
||||||
draggingLink = null;
|
draggingLink = null;
|
||||||
|
|
||||||
|
fireLinkEditEvent();
|
||||||
}
|
}
|
||||||
window.addEventListener("mouseup", endLinkDrag);
|
window.addEventListener("mouseup", endLinkDrag);
|
||||||
window.addEventListener("mousemove", doLinkDrag);
|
window.addEventListener("mousemove", doLinkDrag);
|
||||||
|
|
||||||
|
function linkInput(e) {
|
||||||
|
fireLinkEditEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fireLinkEditEvent() {
|
||||||
|
window.dispatchEvent(new Event("linkedit"));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,20 @@
|
||||||
<script src="{{ static "js/templates.js" }}"></script>
|
<script src="{{ static "js/templates.js" }}"></script>
|
||||||
<script src="{{ static "js/base64.js" }}"></script>
|
<script src="{{ static "js/base64.js" }}"></script>
|
||||||
<script src="{{ static "js/markdown_upload.js" }}"></script>
|
<script src="{{ static "js/markdown_upload.js" }}"></script>
|
||||||
|
<script src="{{ static "go_wasm_exec.js" }}"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const go = new Go();
|
||||||
|
WebAssembly.instantiateStreaming(fetch('{{ static "parsing.wasm" }}'), go.importObject)
|
||||||
|
.then(result => {
|
||||||
|
go.run(result.instance); // don't await this; we want it to be continuously running
|
||||||
|
updateLinkPreviews();
|
||||||
|
});
|
||||||
|
window.parseKnownServicesForUrl = null; // will be set by the Go code
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#desc-preview:empty::after {
|
#desc_preview:empty::after {
|
||||||
content: 'A preview of your description will appear here.';
|
content: 'A preview of your description will appear here.';
|
||||||
color: var(--dimmer-color);
|
color: var(--dimmer-color);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
@ -195,7 +206,38 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="flex-fair bl pa4">
|
<div class="flex-fair bl pa4">
|
||||||
<div id="desc-preview" class="w-100 post-content"></div>
|
<!-- Link / card templates -->
|
||||||
|
<template id="primary_link">
|
||||||
|
<!-- need href -->
|
||||||
|
<a data-tmpl="root" class="ph3 pv2 flex items-center"><span data-tmpl="name"></span><span class="svgicon f6 ml2">{{ svg "arrow-right-up" }}</span></a>
|
||||||
|
</template>
|
||||||
|
<template id="secondary_link">
|
||||||
|
<!-- need href and title -->
|
||||||
|
<a data-tmpl="root" class="ph2 flex"><!-- need icon --></a>
|
||||||
|
</template>
|
||||||
|
<div hidden>
|
||||||
|
{{ range .AllLogos }}
|
||||||
|
<span id="link-icon-{{ .Name }}">{{ .Svg }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
NOTE(ben): This is a copy-paste from project_homepage.html right now.
|
||||||
|
We don't have a good story for sharing templates between Go and JS.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Header image / links -->
|
||||||
|
<div id="header_img_preview" class="project-header-img"><!-- Needs background-image -->
|
||||||
|
<div class="flex justify-end pa3">
|
||||||
|
<div class="flex g3">
|
||||||
|
<div id="primary_links_preview" class="project-links hide-if-empty"></div>
|
||||||
|
<div id="secondary_links_preview" class="project-links ph1 hide-if-empty"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Long description preview -->
|
||||||
|
<div id="desc_preview" class="w-100 post-content"></div>
|
||||||
</div>
|
</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 */}}
|
<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 */}}
|
||||||
|
@ -235,7 +277,7 @@
|
||||||
const projectName = "{{ .Project.Name }}";
|
const projectName = "{{ .Project.Name }}";
|
||||||
{{ end }}
|
{{ end }}
|
||||||
const description = document.querySelector('#full_description');
|
const description = document.querySelector('#full_description');
|
||||||
const descPreview = document.querySelector('#desc-preview');
|
const descPreview = document.querySelector('#desc_preview');
|
||||||
const { clear: clearDescription } = autosaveContent({
|
const { clear: clearDescription } = autosaveContent({
|
||||||
inputEl: description,
|
inputEl: description,
|
||||||
storageKey: `project-description/${projectName}`,
|
storageKey: `project-description/${projectName}`,
|
||||||
|
@ -397,5 +439,64 @@
|
||||||
{{ .TextEditor.UploadUrl }}
|
{{ .TextEditor.UploadUrl }}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/////////////////////
|
||||||
|
// Link management //
|
||||||
|
/////////////////////
|
||||||
|
|
||||||
|
const primaryLinkTemplate = makeTemplateCloner("primary_link");
|
||||||
|
const secondaryLinkTemplate = makeTemplateCloner("secondary_link");
|
||||||
|
|
||||||
|
function updateLinkPreviews() {
|
||||||
|
const links = document.querySelector("#links");
|
||||||
|
const linksChildren = Array.from(links.children);
|
||||||
|
|
||||||
|
const secondaryHeader = links.querySelector(".secondary_links");
|
||||||
|
const rows = links.querySelectorAll(".link_row");
|
||||||
|
|
||||||
|
function index(el) {
|
||||||
|
return linksChildren.indexOf(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryPreview = document.querySelector("#primary_links_preview");
|
||||||
|
const secondaryPreview = document.querySelector("#secondary_links_preview");
|
||||||
|
|
||||||
|
primaryPreview.innerHTML = "";
|
||||||
|
secondaryPreview.innerHTML = "";
|
||||||
|
|
||||||
|
const indexOfSecondary = index(secondaryHeader);
|
||||||
|
for (const row of rows) {
|
||||||
|
const name = row.querySelector(".link_name").value;
|
||||||
|
const url = row.querySelector(".link_url").value;
|
||||||
|
|
||||||
|
const primary = index(row) < indexOfSecondary;
|
||||||
|
if (primary) {
|
||||||
|
const l = primaryLinkTemplate();
|
||||||
|
l.root.href = url;
|
||||||
|
l.name.innerText = name;
|
||||||
|
primaryPreview.appendChild(l.root);
|
||||||
|
} else {
|
||||||
|
let icon = "website";
|
||||||
|
let title = "";
|
||||||
|
if (parseKnownServicesForUrl) {
|
||||||
|
const guess = parseKnownServicesForUrl(url);
|
||||||
|
icon = guess.icon;
|
||||||
|
title = guess.service;
|
||||||
|
if (guess.username) {
|
||||||
|
title += ` (${guess.username})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const iconSVG = document.querySelector(`#link-icon-${icon}`).innerHTML;
|
||||||
|
|
||||||
|
const l = secondaryLinkTemplate();
|
||||||
|
l.root.href = url;
|
||||||
|
l.root.title = name || title;
|
||||||
|
l.root.innerHTML = iconSVG;
|
||||||
|
secondaryPreview.appendChild(l.root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateLinkPreviews();
|
||||||
|
window.addEventListener("linkedit", () => updateLinkPreviews());
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="flex-grow-1 flex flex-column items-center mw-site pt4">
|
<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="project-header-img" style="background-image: url('{{ .Project.HeaderImage }}')">
|
||||||
<div class="flex justify-between pa3 link-normal b">
|
<div class="flex justify-between pa3">
|
||||||
<div class="flex g3">
|
<div class="flex g3">
|
||||||
{{ if .CanEdit }}
|
{{ if .CanEdit }}
|
||||||
<div class="bg-transparent flex">
|
<div class="bg-transparent flex">
|
||||||
|
@ -21,18 +21,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex g3">
|
<div class="flex g3">
|
||||||
{{ with .PrimaryLinks }}
|
{{ with .PrimaryLinks }}
|
||||||
<div class="bg-transparent flex">
|
<div class="project-links">
|
||||||
{{ range . }}
|
{{ range . }}
|
||||||
<a class="ph3 pv2 flex items-center" href="{{ .Url }}">{{ .Name }}<span class="svgicon f6 ml2">{{ svg "arrow-right-up" }}</span></a>
|
<a class="ph3 pv2 flex items-center" href="{{ .Url }}">{{ .Name }}<span class="svgicon f6 ml2">{{ svg "arrow-right-up" }}</span></a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ with .SecondaryLinks }}
|
{{ with .SecondaryLinks }}
|
||||||
<div class="bg-transparent flex items-center ph1">
|
<div class="project-links ph1">
|
||||||
{{ range . }}
|
{{ range . }}
|
||||||
<a class="ph2 flex" href="{{ .Url }}" title="{{ .ServiceName }}{{ with .Username }} ({{ . }}){{ end }}">{{ svg (strjoin "logos/" .Icon) }}</a>
|
<a class="ph2 flex" href="{{ .Url }}" title="{{ .ServiceName }}{{ with .Username }} ({{ . }}){{ end }}">{{ svg (strjoin "logos/" .Icon) }}</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="pv2">​</div><!-- make sure secondary links render at the right height despite SVG size -->
|
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -155,6 +155,23 @@ func GetImg(file string) []byte {
|
||||||
return utils.Must1(io.ReadAll(img))
|
return utils.Must1(io.ReadAll(img))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ListImgsDir(dir string) []fs.DirEntry {
|
||||||
|
var imgs fs.ReadDirFS
|
||||||
|
if config.Config.DevConfig.LiveTemplates {
|
||||||
|
imgs = utils.DirFS("src/templates/img").(fs.ReadDirFS)
|
||||||
|
} else {
|
||||||
|
imgs = Imgs
|
||||||
|
dir = filepath.Join("img/", dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := imgs.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
var controlCharRegex = regexp.MustCompile(`\p{Cc}`)
|
var controlCharRegex = regexp.MustCompile(`\p{Cc}`)
|
||||||
|
|
||||||
var HMNTemplateFuncs = template.FuncMap{
|
var HMNTemplateFuncs = template.FuncMap{
|
||||||
|
@ -247,10 +264,7 @@ var HMNTemplateFuncs = template.FuncMap{
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"svg": func(name string) template.HTML {
|
"svg": func(name string) template.HTML {
|
||||||
contents, err := Imgs.ReadFile(fmt.Sprintf("img/%s.svg", name))
|
contents := GetImg(fmt.Sprintf("%s.svg", name))
|
||||||
if err != nil {
|
|
||||||
panic("SVG not found: " + name)
|
|
||||||
}
|
|
||||||
return template.HTML(contents)
|
return template.HTML(contents)
|
||||||
},
|
},
|
||||||
"static": func(filepath string) string {
|
"static": func(filepath string) string {
|
||||||
|
|
|
@ -228,6 +228,11 @@ type Link struct {
|
||||||
Primary bool `json:"primary"`
|
Primary bool `json:"primary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Icon struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Svg template.HTML `json:"svg"`
|
||||||
|
}
|
||||||
|
|
||||||
type Podcast struct {
|
type Podcast struct {
|
||||||
Title string
|
Title string
|
||||||
Description string
|
Description string
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -372,6 +373,8 @@ type ProjectEditData struct {
|
||||||
APICheckUsernameUrl string
|
APICheckUsernameUrl string
|
||||||
LogoMaxFileSize, HeaderMaxFileSize int
|
LogoMaxFileSize, HeaderMaxFileSize int
|
||||||
|
|
||||||
|
AllLogos []templates.Icon
|
||||||
|
|
||||||
TextEditor templates.TextEditor
|
TextEditor templates.TextEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,7 +399,7 @@ func ProjectNew(c *RequestContext) ResponseData {
|
||||||
currentJam = hmndata.CurrentJam()
|
currentJam = hmndata.CurrentJam()
|
||||||
if currentJam != nil {
|
if currentJam != nil {
|
||||||
project.JamParticipation = []templates.ProjectJamParticipation{
|
project.JamParticipation = []templates.ProjectJamParticipation{
|
||||||
templates.ProjectJamParticipation{
|
{
|
||||||
JamName: currentJam.Name,
|
JamName: currentJam.Name,
|
||||||
JamSlug: currentJam.Slug,
|
JamSlug: currentJam.Slug,
|
||||||
Participating: true,
|
Participating: true,
|
||||||
|
@ -416,6 +419,8 @@ func ProjectNew(c *RequestContext) ResponseData {
|
||||||
LogoMaxFileSize: ProjectLogoMaxFileSize,
|
LogoMaxFileSize: ProjectLogoMaxFileSize,
|
||||||
HeaderMaxFileSize: ProjectHeaderMaxFileSize,
|
HeaderMaxFileSize: ProjectHeaderMaxFileSize,
|
||||||
|
|
||||||
|
AllLogos: allLogos(),
|
||||||
|
|
||||||
TextEditor: templates.TextEditor{
|
TextEditor: templates.TextEditor{
|
||||||
MaxFileSize: AssetMaxSize(c.CurrentUser),
|
MaxFileSize: AssetMaxSize(c.CurrentUser),
|
||||||
UploadUrl: c.UrlContext.BuildAssetUpload(),
|
UploadUrl: c.UrlContext.BuildAssetUpload(),
|
||||||
|
@ -558,6 +563,8 @@ func ProjectEdit(c *RequestContext) ResponseData {
|
||||||
LogoMaxFileSize: ProjectLogoMaxFileSize,
|
LogoMaxFileSize: ProjectLogoMaxFileSize,
|
||||||
HeaderMaxFileSize: ProjectHeaderMaxFileSize,
|
HeaderMaxFileSize: ProjectHeaderMaxFileSize,
|
||||||
|
|
||||||
|
AllLogos: allLogos(),
|
||||||
|
|
||||||
TextEditor: templates.TextEditor{
|
TextEditor: templates.TextEditor{
|
||||||
MaxFileSize: AssetMaxSize(c.CurrentUser),
|
MaxFileSize: AssetMaxSize(c.CurrentUser),
|
||||||
UploadUrl: c.UrlContext.BuildAssetUpload(),
|
UploadUrl: c.UrlContext.BuildAssetUpload(),
|
||||||
|
@ -1102,3 +1109,16 @@ func CanEditProject(user *models.User, owners []*models.User) bool {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func allLogos() []templates.Icon {
|
||||||
|
var logos []templates.Icon
|
||||||
|
logoEntries := templates.ListImgsDir("logos")
|
||||||
|
for _, logo := range logoEntries {
|
||||||
|
logos = append(logos, templates.Icon{
|
||||||
|
Name: logo.Name()[:len(logo.Name())-len(".svg")],
|
||||||
|
Svg: template.HTML(templates.GetImg(fmt.Sprintf("logos/%s", logo.Name()))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return logos
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue