Browse Source

Create a sharing link using the slug now and keep the admin from accidentally submitting on ENTER.

dev
Zed A. Shaw 1 week ago
parent
commit
438f7b0a90
  1. 3
      api/admin/clips.js
  2. 2
      client/components/ClipScroll.svelte
  3. 25
      client/pages/ClipPlayer.svelte
  4. 2
      client/pages/Clips.svelte
  5. 14
      client/pages/admin/Clips.svelte
  6. 13
      lib/models.js
  7. 15
      migrations/20211122202014_media_add_slug.cjs
  8. 18
      rendered/pages/clips/[slug]/index.svelte

3
api/admin/clips.js

@ -66,9 +66,10 @@ export const post = async (req, res) => {
} else {
const { clip } = req.body;
assert(clip.id, "Id is required.");
clip.slug = Media.format_slug(clip);
// do the upsert
const result = await Media.upsert(Media.clean(clip), "id");
await Media.upsert(Media.clean(clip), "id");
return api.reply(200, {message: "OK"});
}
} catch (error) {

2
client/components/ClipScroll.svelte

@ -126,7 +126,7 @@
<span><i><b>{ clip.title }</b></i></span>
<span>{ formatDistance(new Date(clip.created_at), new Date()) } ago</span>
<span>{ clip.views } views</span>
<Icon name="thumbs-up" color="var(--color-accent)" size="24" />
<Icon name="share" color="var(--color-accent)" size="24" />
</top>
<middle>
<IsVisible on:visible={ () => show_video(clip) }

25
client/pages/ClipPlayer.svelte

@ -8,11 +8,19 @@
import Layout from '$/client/Layout.svelte';
import api from "$/client/api.js";
import { defer } from "$/client/helpers.js";
import { base_host } from "$/client/config.js";
import { fade } from "svelte/transition";
export let params = {};
let link_copied = false;
let { clip_id } = params;
const clip_promise = defer();
const copy_share_link = (clip) => {
link_copied = `${base_host}/clips/${clip.slug}/`;
navigator.clipboard.writeText(link_copied);
}
onMount(async () => {
const [status, data] = await api.get(`/api/clip?id=${ clip_id }`);
@ -29,6 +37,7 @@
info {
display: flex;
flex-direction: column;
width: 100%;
}
info stats {
@ -42,11 +51,15 @@
info description {
padding: 1rem;
padding-top: 1rem;
display: flex;
flex-direction: column;
font-size: 1.3em;
}
toast {
background-color: var(--color);
color: var(--color-bg);
}
</style>
<Layout centered={ true } fullwidth={ true }>
@ -70,7 +83,7 @@
<span><i><b>{ clip.title }</b></i></span>
<span>{ formatDistance(new Date(clip.created_at), new Date()) } ago</span>
<span>{ clip.views } views</span>
<Icon name="thumbs-up" color="var(--color-accent)" size="24" />
<span on:click={ () => copy_share_link(clip) }><Icon name="share" color="var(--color-accent)" size="24" /></span>
</stats>
<description>
@ -83,6 +96,14 @@
<a href="/" use:link>Return the to listing.</a>
</callout>
{/await}
{#if link_copied}
<toast-list class="bottom right active">
<toast transition:fade|local>
<p>Sharing link copied. <a href="{ link_copied }" target="_"><Icon name="external-link" color="var(--color-bg)" /></a></p>
</toast>
</toast-list>
{/if}
</Layout>

2
client/pages/Clips.svelte

@ -183,7 +183,7 @@
<sidebar class="small">
<span><a href="/" use:link><Icon name="video" /></a></span>
<span><a href="/" use:link><Icon name="log-in" /></a></span>
<span><a href="/login/" use:link><Icon name="log-in" /></a></span>
<span><a href="https://twitter.com/lzsthw"><Icon name="twitter" /></a></span>
<span><a href="https://instagram.com/zedshaw"><Icon name="instagram" /></a></span>
<span><a href="https://twitch.tv/zedashaw"><Icon name="twitch" /></a></span>

14
client/pages/admin/Clips.svelte

@ -189,9 +189,9 @@
<Layout authenticated={ false } centered={ true } fullwidth={ true } footer={ false } header={ true } fixed={ true }>
<content>
<button-group>
<button class:selected={ state === "new" } on:click|preventDefault={ () => change_state("new") }>New Clips</button>
<button class:selected={ state === "edited" } on:click|preventDefault={ () => change_state("edited") }>Edited Clips</button>
<button class:selected={ state === "published" } on:click|preventDefault={ () => change_state("published") }>Published Clips</button>
<button type="button" class:selected={ state === "new" } on:click|preventDefault={ () => change_state("new") }>New Clips</button>
<button type="button" class:selected={ state === "edited" } on:click|preventDefault={ () => change_state("edited") }>Edited Clips</button>
<button type="button" class:selected={ state === "published" } on:click|preventDefault={ () => change_state("published") }>Published Clips</button>
</button-group>
{#key state}
@ -234,20 +234,20 @@
</IsVisible>
</middle>
<bottom>
<form action="/api/admin/clips" method="POST">
<form on:submit|preventDefault={ () => update_clip(clip, "edited") } action="/api/admin/clips" method="POST">
<input bind:value={ clip.title } name="title" placeholder="Title" />
<textarea bind:value={ clip.description } name="description" placeholder="Description" ></textarea>
<input bind:value={ clip.tags } name="tags" placeholder="Tags" />
<button-group>
<button on:click|preventDefault={ () => delete_clip(clip) }>Delete</button>
<button on:click|preventDefault={ () => update_clip(clip, "edited") }>
<button type="button" on:click|preventDefault={ () => delete_clip(clip) }>Delete</button>
<button type="button" on:click|preventDefault={ () => update_clip(clip, "edited") }>
{#if state === "published"}
Unpublish
{:else}
Update
{/if}
</button>
<button on:click|preventDefault={ () => update_clip(clip, "published") }>
<button type="button" on:click|preventDefault={ () => update_clip(clip, "published") }>
{#if state === "published"}
Update
{:else}

13
lib/models.js

@ -4,6 +4,7 @@ import {v4 as uuid} from "uuid";
import logging from "./logging.js";
import assert from "assert";
import crypto from "crypto";
import slugify from "slugify";
const log = logging.create("lib/models.js");
@ -145,6 +146,18 @@ export class UserPayment extends Model.from_table("user_payment") {
}
export class Media extends Model.from_table("media") {
static format_slug(clip) {
let title_slug = slugify(clip.title, {
replacement: '-', // replace spaces with replacement character, defaults to `-`
remove: /:/, // remove characters that match regex, defaults to `undefined`
lower: true, // convert to lower case, defaults to `false`
strict: true, // strip special characters except replacement, defaults to `false`
locale: 'us', // language code of the locale to use
trim: true // trim leading and trailing replacement chars, defaults to `true`
});
return `${clip.id}-${title_slug}`;
}
}

15
migrations/20211122202014_media_add_slug.cjs

@ -0,0 +1,15 @@
exports.up = async (knex) => {
// using async/await lets you work with multiple tables in one up/down
await knex.schema.alterTable('media', (table) => {
table.string("slug");
});
// because this is an async it'll automatically be a promise for knex
// add any other tables here with an await, no need to return
}
exports.down = async (knex) => {
await knex.schema.alterTable('media', (table) => {
table.dropColumn("slug");
});
}

18
rendered/pages/clips/[slug]/index.svelte

@ -1,23 +1,12 @@
<script context="module">
import fs from "fs";
import slugify from "slugify";
const clips = JSON.parse(fs.readFileSync("../static/clips/index.json"));
export const getPaths = async () => {
return clips.map(clip => {
let title = slugify(clip.title, {
replacement: '-', // replace spaces with replacement character, defaults to `-`
remove: /:/, // remove characters that match regex, defaults to `undefined`
lower: true, // convert to lower case, defaults to `false`
strict: true, // strip special characters except replacement, defaults to `false`
locale: 'us', // language code of the locale to use
trim: true // trim leading and trailing replacement chars, defaults to `true`
});
let slug = `${clip.id}-${title}`;
return { slug, clip};
return { slug: clip.slug, clip};
});
}
@ -75,10 +64,11 @@
info {
display: flex;
flex-direction: column;
padding: 1rem;
width: 100%;
}
info stats {
padding: 0.5rem;
display: flex;
flex-direction: row;
justify-content: space-around;
@ -87,7 +77,7 @@
}
info description {
padding-top: 1rem;
padding: 1rem;
display: flex;
flex-direction: column;
font-size: 1.3em;

Loading…
Cancel
Save