This commit is contained in:
parent
369453794b
commit
594f87f240
14 changed files with 226 additions and 43 deletions
Binary file not shown.
|
@ -15,6 +15,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"jdenticon": "^3.3.0",
|
||||
"lorem-ipsum": "^2.0.8",
|
||||
"node-vibrant": "^4.0.3",
|
||||
"pinia": "^3.0.3",
|
||||
"sass": "^1.89.2",
|
||||
|
|
|
@ -5,21 +5,23 @@ import Header from '@/components/global/Header.vue'
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<Header />
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header {
|
||||
line-height: 1.5;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
|
@ -28,7 +30,7 @@ nav {
|
|||
}
|
||||
|
||||
nav a.router-link-exact-active {
|
||||
color: var(--color-text);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active:hover {
|
||||
|
@ -38,7 +40,7 @@ nav a.router-link-exact-active:hover {
|
|||
nav a {
|
||||
display: inline-block;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid var(--color-border);
|
||||
border-left: 1px solid var(--text);
|
||||
}
|
||||
|
||||
nav a:first-of-type {
|
||||
|
@ -52,9 +54,6 @@ nav a:first-of-type {
|
|||
padding-right: calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0 2rem 0 0;
|
||||
}
|
||||
|
||||
header .wrapper {
|
||||
display: flex;
|
||||
|
|
|
@ -25,4 +25,26 @@ svg.black-white {
|
|||
stroke: var(--text);
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Copied from: https://stackoverflow.com/a/4407335 */
|
||||
.no-select {
|
||||
-webkit-touch-callout: none; /* iOS Safari */ /* Safari */ /* Konqueror HTML */ /* Old versions of Firefox */ /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently
|
||||
supported by Chrome, Edge, Opera and Firefox */
|
||||
}
|
||||
|
||||
.separator-horizontal {
|
||||
width: 100%;
|
||||
border: 1px solid black;
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
.filling-spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=base.css.map */
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"version":3,"sourceRoot":"","sources":["base.scss"],"names":[],"mappings":"AAGA;EACC;IACC;IACA;IACA;IACA;IACA;;;AAIF;EACC;IACC;IACA;IACA;IACA;IACA;;;AAKF;EACE;;;AAGF;EACI;EACA","file":"base.css"}
|
||||
{"version":3,"sourceRoot":"","sources":["base.scss"],"names":[],"mappings":"AAGA;EACC;IACC;IACA;IACA;IACA;IACA;;;AAIF;EACC;IACC;IACA;IACA;IACA;IACA;;;AAKF;EACE;;;AAGF;EACI;EACA;;;AAGJ;EACE;;;AAGF;AACA;EACE;EACA;AAAA;;;AAIF;EACE;EACA;EACA;EACA;;;AAGF;EACE","file":"base.css"}
|
|
@ -30,3 +30,25 @@ svg.black-white {
|
|||
fill: var(--text);
|
||||
stroke: var(--text);
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Copied from: https://stackoverflow.com/a/4407335 */
|
||||
.no-select {
|
||||
-webkit-touch-callout: none; /* iOS Safari */ /* Safari */ /* Konqueror HTML */ /* Old versions of Firefox */ /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently
|
||||
supported by Chrome, Edge, Opera and Firefox */
|
||||
}
|
||||
|
||||
.separator-horizontal {
|
||||
width: 100%;
|
||||
border: 1px solid black;
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
.filling-spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
|
19
frontend-vue/src/components/global/ScreenreaderMenu.vue
Normal file
19
frontend-vue/src/components/global/ScreenreaderMenu.vue
Normal file
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
// A menu intended for placement at the very beginning of the page.
|
||||
// It's main purpose is to aid and improve screenreader support and keyboard navigation
|
||||
// It should only be visible when explicitly focused by the browser
|
||||
// (as the first element, this should always be the case for keyboard navigation when loading the page)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="screenreader-menu-wrapper"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.screenreader-menu-wrapper {
|
||||
display: none;
|
||||
}
|
||||
.screenreader-menu-wrapper:focus {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
|
@ -1,40 +1,85 @@
|
|||
<script setup lang="ts">
|
||||
import PreviewHeader from '@/components/user/PreviewHeader.vue'
|
||||
import type { Note as NoteData } from '@/stores/note.ts'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import FormattedText from '@/components/util/FormattedText.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
note: NoteData;
|
||||
note: NoteData
|
||||
}>()
|
||||
|
||||
let collapsed = ref<boolean>(true);
|
||||
const MaxNoCollapseLen = 200
|
||||
|
||||
const collapsed = ref<boolean>(true)
|
||||
const collapsable = props.note.rawContent.length > MaxNoCollapseLen
|
||||
const collapseButtonText = computed(() => {
|
||||
return collapsed.value ? 'Expand' : 'Collapse'
|
||||
})
|
||||
|
||||
function shortenContent(): string {
|
||||
if (!collapsed.value) return props.note.rawContent;
|
||||
if (props.note.rawContent.length > 200) {
|
||||
const words = props.note.rawContent.split(' ');
|
||||
let outString = '';
|
||||
if (!collapsed.value) return props.note.rawContent
|
||||
if (props.note.rawContent.length > MaxNoCollapseLen) {
|
||||
const words = props.note.rawContent.split(' ')
|
||||
let outString = ''
|
||||
for (const word of words) {
|
||||
if (outString.length > 200) {
|
||||
return outString + '...';
|
||||
if (outString.length > MaxNoCollapseLen) {
|
||||
return outString + '...'
|
||||
}
|
||||
outString += word + ' ';
|
||||
outString += word + ' '
|
||||
}
|
||||
return outString;
|
||||
return outString
|
||||
} else {
|
||||
return props.note.rawContent;
|
||||
return props.note.rawContent
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExpansion($el: HTMLElement) {
|
||||
collapsed.value = !collapsed.value;
|
||||
if (!collapsed.value) return;
|
||||
$el.scrollIntoView();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div role="document" class="note">
|
||||
<!-- Preview for reply here, decide if this component can also serve as such a preview -->
|
||||
<PreviewHeader :user="props.note.owner" />
|
||||
<FormattedText :raw-text="shortenContent" :hashtags="props.note.hashtags" :pinged="props.note.pings" />
|
||||
<FormattedText
|
||||
:raw-text="shortenContent()"
|
||||
:hashtags="props.note.hashtags"
|
||||
:pinged="props.note.pings"
|
||||
/>
|
||||
<div class="toggle-wrapper" v-if="collapsable">
|
||||
<div
|
||||
type="button"
|
||||
@click="toggleExpansion($el)"
|
||||
class="collapse-toggle no-select"
|
||||
role="button"
|
||||
aria-description="Toggle to expand/collapse the note content. Default is collapsed"
|
||||
tabindex="0"
|
||||
>
|
||||
{{ collapseButtonText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.note {
|
||||
background-color: var(--secondary);
|
||||
}
|
||||
|
||||
.collapse-toggle {
|
||||
color: var(--text);
|
||||
border: var(--text) 1px solid;
|
||||
border-radius: 1em;
|
||||
background-color: var(--accent);
|
||||
padding: 0.2em 0.6em 0.2em 0.6em;
|
||||
}
|
||||
|
||||
.toggle-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,18 +1,33 @@
|
|||
<script setup lang="ts">
|
||||
import { defineProps } from "vue"
|
||||
import { defineProps } from 'vue'
|
||||
import type { User } from '../../stores/userdata'
|
||||
import ProfilePicture from '@/components/user/ProfilePicture.vue'
|
||||
import FormattedText from '@/components/util/FormattedText.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
user: User
|
||||
}>()
|
||||
</script>
|
||||
<template>
|
||||
<div class="account-header">
|
||||
<ProfilePicture :username="props.user.username" :media="props.user.profilePicture" />
|
||||
<div class="pfp-wrapper">
|
||||
<ProfilePicture
|
||||
class="pfp"
|
||||
:size="48"
|
||||
:username="props.user.username"
|
||||
:media="props.user.profilePicture"
|
||||
/>
|
||||
</div>
|
||||
<div class="account-info">
|
||||
<FormattedText :raw-text="props.user.displayName"/>
|
||||
<p>{{props.user.username}}</p>
|
||||
<FormattedText class="displayname" :raw-text="props.user.displayName" />
|
||||
<p class="username">{{ props.user.username }}</p>
|
||||
</div>
|
||||
<div class="filler"/>
|
||||
<!-- TODO: Distinguish this from buttons.
|
||||
It is not a button. It is not clickable. It must not look like the clickable things
|
||||
-->
|
||||
<div class="server-wrapper">
|
||||
<p class="server-name">{{props.user.server.name}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -21,10 +36,35 @@ import FormattedText from '@/components/util/FormattedText.vue'
|
|||
.account-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 4em;
|
||||
background-color: var(--accent);
|
||||
padding: 0 0.6em 0.6em 0.6em;
|
||||
}
|
||||
.account-header {
|
||||
|
||||
.pfp {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.pfp-wrapper {
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.displayname {
|
||||
font-weight: bold;
|
||||
}
|
||||
.username {
|
||||
margin-top: -0.75em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.filler {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.server-name {
|
||||
border: var(--text) 1px solid;
|
||||
border-radius: 0.6em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { vi } from 'vitest'
|
|||
const props = defineProps<{
|
||||
media?: MediaMetadata
|
||||
username: string
|
||||
size: number
|
||||
}>()
|
||||
|
||||
/* async function calcBackgroundColor() {
|
||||
|
@ -45,7 +46,7 @@ async function calcBackgroundStyle() {
|
|||
Get primary color of the image, invert it and set as background for higher contrast in case of transparency
|
||||
calcBackgroundStyle *should* do it, but async and vue no like
|
||||
-->
|
||||
<img :src="props.media.url" :alt="props.media.alt" height="52" width="52" ref="profilePicture"/>
|
||||
<img :src="props.media.url" :alt="props.media.alt" :height="props.size" :width="props.size" ref="profilePicture"/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
|
|
|
@ -12,10 +12,17 @@
|
|||
// Probably something akin to the treeificator. Marshal text into tree first,
|
||||
// then adjust tree nodes to types that unmarshal into html
|
||||
// Or single pass transform, keeping a stack of the current context
|
||||
// If stack isn't empty at the end, log an error to console, complete with required closures in order
|
||||
//
|
||||
// IMPORTANT: This function is security critical!
|
||||
// The content it produces is rendered out directly, with no further protections.
|
||||
// However, since the only content to parse would be server-produced Linstrom formatted text,
|
||||
// there should not be much of an issue here (unless the client is directly modified, at which
|
||||
// point all bets are off the table) (the burden of securing the input is on the server)
|
||||
function render(): string {
|
||||
return props.rawText
|
||||
return "<p>"+props.rawText+"</p>"
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div v-html="render" />
|
||||
<div v-html="render()" />
|
||||
</template>
|
||||
|
|
7
frontend-vue/src/stores/remoteServer.ts
Normal file
7
frontend-vue/src/stores/remoteServer.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export interface RemoteServer {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
isSelf: boolean;
|
||||
isDead: boolean;
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import type { MediaMetadata } from '@/stores/media.ts'
|
||||
import type { RemoteServer } from '@/stores/remoteServer.ts'
|
||||
|
||||
export interface User {
|
||||
username: string
|
||||
|
@ -7,4 +8,5 @@ export interface User {
|
|||
profilePicture?: MediaMetadata
|
||||
bannerPicture?: MediaMetadata
|
||||
backgroundPicture?: MediaMetadata
|
||||
server: RemoteServer
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import type { MediaMetadata } from '@/stores/media.ts'
|
||||
import type { User } from '@/stores/userdata.ts'
|
||||
import PreviewHeader from '@/components/user/PreviewHeader.vue'
|
||||
import type { Note as NoteData } from '@/stores/note.ts'
|
||||
import Note from '@/components/note/Note.vue'
|
||||
import { loremIpsum } from 'lorem-ipsum'
|
||||
import type { RemoteServer } from '@/stores/remoteServer.ts'
|
||||
|
||||
const sampleMedia = {
|
||||
url: "https://valco.fi/cdn/shop/files/nettisivu_valkoinen.webp",
|
||||
url: "https://mk.absturztau.be/proxy/avatar.webp?url=https%3A%2F%2Fmisskey-taube.s3.eu-central-1.wasabisys.com%2Ffiles%2F40dc29f1-bb92-4df1-a4b7-011287456cf3.webp&avatar=1",
|
||||
alt: "valko logo",
|
||||
blurred: false,
|
||||
mediaType: "image/webp",
|
||||
|
@ -13,15 +16,30 @@
|
|||
remote: false,
|
||||
id: "some-media-api"
|
||||
} as MediaMetadata;
|
||||
const server = {
|
||||
isDead: false,
|
||||
isSelf: false,
|
||||
url: "example.com",
|
||||
name: "example",
|
||||
id: "some-server-id"
|
||||
} as RemoteServer;
|
||||
const user = {
|
||||
username: "valko",
|
||||
displayName: "valko",
|
||||
profilePicture: sampleMedia
|
||||
profilePicture: sampleMedia,
|
||||
server: server,
|
||||
} as User;
|
||||
const note = {
|
||||
rawContent: loremIpsum({count: 100}),
|
||||
hashtags: [],
|
||||
owner: user,
|
||||
pings: [],
|
||||
id: "some-note-id"
|
||||
} as NoteData;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PreviewHeader :user="user" />
|
||||
<Note :note="note" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue