wip: Discord Server Rules page, fixed ESLint restriction on link components, added linking to specific element ids on internal routes

This commit is contained in:
Benjamin Singleton 2026-02-13 23:55:39 -06:00
parent de86791e2a
commit feacb9b754
11 changed files with 350 additions and 42 deletions

View file

@ -14,6 +14,7 @@ export default defineConfig([
...vue.configs["flat/essential"],
],
files: ["./src/**/*.{js,ts,vue}"],
plugins: { vue },
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
@ -27,11 +28,11 @@ export default defineConfig([
},
},
rules: {
"vue/no-restricted-component-names": [
"vue/no-restricted-html-elements": [
"error",
{
name: "RouterLink",
message: "Use SmartLink instead of RouterLink.",
element: ["a", "RouterLink"],
message: "Use <SmartLink> instead",
},
],
// allow interfaces to only extend another interface without adding properties

View file

@ -1,15 +1,19 @@
<!doctype html>
<html lang="en" class="has-navbar-fixed-top">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/viossa_circle.svg"/>
<link href='https://cdn.boxicons.com/fonts/basic/boxicons.min.css' rel='stylesheet'>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Viossa.net</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/viossa_circle.svg" />
<link href='https://cdn.boxicons.com/fonts/basic/boxicons.min.css' rel='stylesheet'>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Viossa.net</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View file

@ -4,7 +4,7 @@ import { computed, ref, type Ref } from "vue";
import LocalePicker from "./components/organisms/LocalePicker.vue";
import { vOnClickOutside } from "@vueuse/components";
import { useLocale } from "./i18n";
import { RouterLink, useRouter } from "vue-router";
import { useRouter } from "vue-router";
import SmartLink from "./components/organisms/SmartLink.vue";
import type { SmartDest } from "./utils/smart-dest";
import type { Locale } from "./i18n/locale";
@ -44,13 +44,16 @@ const navbarItems = computed(() =>
const to = ((): SmartDest => {
switch (id) {
case "whatIsViossa": {
return { type: "internal", internal: "/" };
return { type: "internal", internal: { route: "/" } };
}
case "resources": {
return { type: "internal", internal: "/resources" };
return {
type: "internal",
internal: { route: "/resources" },
};
}
case "kotoba": {
return { type: "internal", internal: "/kotoba" };
return { type: "internal", internal: { route: "/kotoba" } };
}
}
})();
@ -68,9 +71,11 @@ const navbarItems = computed(() =>
role="navigation"
aria-label="main navigation">
<div class="navbar-brand">
<RouterLink class="navbar-item has-text-weight-bold" to="/"
><img src="@/assets/ViossaFlagRect.svg" alt=""
/></RouterLink>
<SmartLink
class="navbar-item has-text-weight-bold"
:to="{ type: 'internal', internal: { route: '/' } }">
<img src="@/assets/ViossaFlagRect.svg" alt="" />
</SmartLink>
<div class="navbar-item is-hidden-desktop">
<button

View file

@ -17,10 +17,12 @@
--bulma-info-l: 50%;
--bulma-info-s: 45%;
--bulma-family-primary: Nunito, Inter, SF Pro, Segoe UI, Roboto, Oxygen, Ubuntu, Helvetica Neue, Helvetica, Arial, sans-serif;
--bulma-family-secondary: Nunito, Inter, SF Pro, Segoe UI, Roboto, Oxygen, Ubuntu, Helvetica Neue, Helvetica, Arial, sans-serif;
--bulma-body-family: Nunito, Inter, SF Pro, Segoe UI, Roboto, Oxygen, Ubuntu, Helvetica Neue, Helvetica, Arial, sans-serif;
}
--bulma-family-primary: Nunito,Inter,SF Pro,Segoe UI,Roboto,Oxygen,Ubuntu,Helvetica Neue,Helvetica,Arial,sans-serif;
--bulma-family-secondary: Nunito,Inter,SF Pro,Segoe UI,Roboto,Oxygen,Ubuntu,Helvetica Neue,Helvetica,Arial,sans-serif;
--bulma-body-family: Nunito,Inter,SF Pro,Segoe UI,Roboto,Oxygen,Ubuntu,Helvetica Neue,Helvetica,Arial,sans-serif;
};
// Correct for fixed-top header when jumping to element by id in link (anchor)
html {
scroll-padding-top: var(--bulma-navbar-height);
}

View file

@ -1,16 +1,17 @@
<script setup lang="ts">
import type { SmartDest, SmartInternalDest } from "../../utils/smart-dest"; // needs to be relative for vue sfc compiler
import type { CssClass } from "@/utils/css";
import type { SmartDest } from "../../utils/smart-dest"; // needs to be relative for vue sfc compiler
import { computed, ref } from "vue";
export interface SmartLinkProps {
to: SmartDest;
newTab?: boolean;
covert?: boolean;
}
const props = defineProps<SmartLinkProps>();
type To =
| { type: "a"; a: string }
| { type: "routerLink"; routerLink: SmartInternalDest };
type To = { type: "a"; a: string } | { type: "routerLink"; routerLink: string };
const to = ((): To => {
const { to, newTab } = props;
@ -19,25 +20,43 @@ const to = ((): To => {
return { type: "a", a: to.external };
}
case "internal": {
if (newTab) {
return { type: "a", a: to.internal };
const { route, id } = to.internal;
const endpoint = `${route ?? ""}${id === undefined ? "" : `#${id}`}`;
if (newTab || id !== undefined) {
return { type: "a", a: endpoint };
}
return { type: "routerLink", routerLink: to.internal };
// <RouterLink> can only be used for route-only endpoints
// that require no other special functionality of <a>
return { type: "routerLink", routerLink: endpoint };
}
}
})();
const isHovered = ref(false);
const classes = computed<CssClass>(() => [
props.covert && !isHovered.value && "has-text-text",
]);
</script>
<template>
<!-- eslint-disable-next-line vue/no-restricted-html-elements - we need to at least use <a> once to create this wrapper type -->
<a
v-if="to.type === 'a'"
:href="to.a"
:target="newTab ? '_blank' : undefined"
rel="noopener noreferrer nofollow">
rel="noopener noreferrer nofollow"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
:class="classes">
<slot />
</a>
<RouterLink v-else :to="to.routerLink">
<slot />
<!-- eslint-disable-next-line vue/no-restricted-html-elements - we need to at least use <RouterLink> once to create this wrapper type -->
<RouterLink v-else :to="to.routerLink" :class="classes">
<span @mouseenter="isHovered = true" @mouseleave="isHovered = false">
<slot />
</span>
</RouterLink>
</template>

View file

@ -20,6 +20,7 @@ export interface LocaleMask {
home: HomePage;
resources: ResourcesPage;
kotoba: KotobaPage;
discord: Discord;
}
export interface VilanticLangs extends Record<VilanticId, string> {}
@ -74,3 +75,29 @@ export interface Image {
src: string | Fallback; // fallback can be used if image doesn't need to be translated
alt: string;
}
export interface Discord {
rulesPage: DiscordRulesPage;
}
export interface DiscordRulesPage {
title: string;
overview: DiscordRulesPageOverview;
rules: DiscordRules;
}
export interface DiscordRulesPageOverview {
title: string;
help: string;
}
export interface DiscordRules extends Record<"noTranslation", DiscordRule> {}
export interface DiscordRule {
overview: DiscordRuleOverview;
}
export interface DiscordRuleOverview {
text: string;
subtext: string | null;
}

View file

@ -46,4 +46,13 @@ export default {
title: "Tropos-agnostic search",
searchHelp: "To searcn tropos-agnostically, enter a term below.",
},
discord: {
rulesPage: {
title: "Discord Server Rules",
overview: {
title: "Overview",
help: "Click any rule to see details.",
},
},
},
} as const satisfies Locale;

View file

@ -1 +1,237 @@
<template>Rules</template>
<script setup lang="ts">
import SmartLink from "@/components/organisms/SmartLink.vue";
import { useLocale } from "@/i18n";
const locale = useLocale();
</script>
<template>
<div>
<section class="section">
<h1 class="title">{{ locale.discord.rulesPage.title }}</h1>
</section>
<section class="section content">
<h2>{{ locale.discord.rulesPage.overview.title }}</h2>
<blockquote>
{{ locale.discord.rulesPage.overview.help }}
</blockquote>
<ol>
<SmartLink
covert
:to="{ type: 'internal', internal: { id: 'rule-1' } }">
<li>
No translation! Do not translate to/from Viossa on the
server, except the big four translatables (you can learn
in hard mode without them!)
</li>
</SmartLink>
<SmartLink
covert
:to="{ type: 'internal', internal: { id: 'rule-2' } }">
<li>If it's understood, it's Viossa.</li>
</SmartLink>
<SmartLink
covert
:to="{ type: 'internal', internal: { id: 'rule-3' } }">
<li>
The chats in the Viossa Only category are Viossa only.
</li>
</SmartLink>
<SmartLink
covert
:to="{ type: 'internal', internal: { id: 'rule-4' } }">
<li>
This server is SFW. No sexually explicit, gory, or
violent content.
</li>
</SmartLink>
<SmartLink
covert
:to="{ type: 'internal', internal: { id: 'rule-5' } }">
<li>Don't use hate speech, and respect each other.</li>
</SmartLink>
<SmartLink
covert
:to="{ type: 'internal', internal: { id: 'rule-6' } }">
<li>
Respect the rulings of the staff (<b>@Yewald</b> and
<b>@Yewaldnen</b>).
</li>
</SmartLink>
<SmartLink
covert
:to="{ type: 'internal', internal: { id: 'rule-7' } }">
<li>
Discussion of controversial topics (politics, war, etc.)
should be directed to #polite, which requires the @Ike
role to view, which is itself locked behind
<b>@Viossadjin</b> and <b>@mellandjin</b>.
<ul class="mt-0">
<li>
<b>#feels-and-advice</b> is for talking about
your feelings openly, but we draw the line at
suicidal or violent ideation. These are trains
of thought to be brought to a therapist, and are
not jokes. Because of their seriousness, they
simply don't belong here.
</li>
</ul>
</li>
</SmartLink>
</ol>
</section>
<section class="section content" id="rule-1">
<h2>Rule 1: No translation</h2>
<p>
Translation is not how we learn and teach Viossa. Instead, we
teach using pictures, diagrams, video calls, and other aids to
couple words to meaning.
</p>
<p>
On the Viossa Diskordserver, you are allowed to translate the
following four words. If you want an extra challenge, don't
unspoiler the text:
</p>
<blockquote>TODO - big 4</blockquote>
<p>
Outside of the teaching-learning cycle, we also make an
exception for artistic translations (such as those of songs,
books, or poems), as well as for academic translations (such as
for a formal research paper). In both cases, this exception is
dependent on translations of either class appearing in the
appropriate place. If you're not sure where that is, please ask.
</p>
<p>
Additionally, please don't attempt to derive or share
translation-based learning materials on-server, or poach members
for such a purpose.
</p>
</section>
<section class="section content" id="rule-2">
<h2>Rule 2: If it's understood, it's Viossa</h2>
<p>
All that is required to speak Viossa is that other speakers be
able to understand you. There is no right or wrong way to speak
or write, and no global standard.
</p>
<p>
However, Viossa is a collaborative group project: members should
strive to make others understand them, and in return make an
effort to understand others.
</p>
</section>
<section class="section content" id="rule-3">
<h2>Rule 3: Viossa-only chats</h2>
<p>
Chats in the Viossa Only section do not permit English. If you
must use English to coach learners on the learning process, go
to <b>#meta</b> instead.
</p>
<p>
This doesn't mean that other channels are English-only, though!
Viossa is allowed everywhere.
</p>
</section>
<section class="section content" id="rule-4">
<h2>Rule 4: SFW</h2>
<p>
If a mod does not like what you have posted, they will inform
you; see
<SmartLink
:to="{ type: 'internal', internal: { id: 'rule-6' } }"
>Rule 6</SmartLink
>. This is a public Discord server; think before you post.
</p>
</section>
<section class="section content" id="rule-5">
<h2>Rule 5: Respect one another</h2>
<p>
Respect one another. Using slurs or hate speech against others,
whether on- or off-server, or advocating for violence are not
welcome. This is an LGBTQ+ friendly international community.
</p>
</section>
<section class="section content" id="rule-6">
<h2>Rule 6: Respect the staff's rulings</h2>
<p>
The word of staff (the Yewald as well as the Yewaldnen) is
final, and they may kick, ban, or mute members or change
members' access permissions to make sure this environment stays
respectful and puts the Viossa community first.
</p>
<p>
Appeals will always be considered, and if you feel that a mod
action was inappropriate, you can DM any Yewald or open a ticket
with YAGPDB's /tickets open command.
</p>
<p>
If you are banned, there will be instructions on how to appeal
the ban, however, please take the time to reflect on the ban
reason before appealing.
</p>
</section>
<section class="section content" id="rule-7">
<h2>Rule 7: #polite and ike</h2>
<p>
Many are life's troubling realities, and vast is our need to
discuss them. The ike category is an opt-in set of chats where
discussion of heavy, sensitive, or potentially contentious
topics is allowed, provided that users are especially respectful
of each other during such discussions. By accepting the ike
role, you agree to adhere to this rule and encourage others to
do the same.
</p>
<h3>Venting vs seeking advice</h3>
<p>
Sometimes you want to let people know that you're dealing with
an issue and just be acknowledged, other times you want help in
solving a problem. If you are open to one but not the other,
it's often a good idea to let people know as part of the
discussion so that you can receive the kind of responses you are
looking for.
</p>
<h3>Self-harm and Violence</h3>
<p>
While discussing self-harm in general is allowed (with
appropriate and clear use of content warnings), this server in
itself is not an emergency mental health resource, and is not a
substitute for professional help. Asking others for advice in
finding support or resources off-server is fine, but asking
others to participate in talking you down is inappropriate.
</p>
<p>You should not use this space to:</p>
<ul>
<li>express intent or desire to harm yourself or others</li>
<li>
solicit help in stopping yourself from harming yourself or
someone else
</li>
</ul>
<p>
By crossing these boundaries, please be aware that you are
asking members of the server (including moderators and the
owner) to perform a role for which they are not trained or
equipped. At the moderators' sole discretion, this may not be
tolerated and may result in a warning, timeout, removal of the
@ike role, or removal from the server.
</p>
<p>
If you are struggling with thoughts of this nature, but are not
immediately in danger, please consider seeking counseling. If
you are experiencing an immediate crisis, please call 988 (in
the United States), 999 (in the UK), or locate an emergency
hotline appropriate for you. A list of resources by country
exists here:
<SmartLink
:to="{
type: 'external',
external:
'https://blog.opencounseling.com/suicide-hotlines/',
}"
new-tab
>https://blog.opencounseling.com/suicide-hotlines/</SmartLink
>
</p>
</section>
</div>
</template>

View file

@ -40,7 +40,9 @@ const computeButtons = (
style: { color: "primary" },
},
{
link: { to: { type: "internal", internal: "/discord/rules" } },
link: {
to: { type: "internal", internal: { route: "/discord/rules" } },
},
label: buttons.rules.label,
style: { color: "warning", outlined: true },
},

View file

@ -1 +1 @@
export type CssClass = string | false | null | undefined;
export type CssClass = string | false | null | undefined | CssClass[];

View file

@ -4,5 +4,8 @@ export type SmartDest =
| { type: "internal"; internal: SmartInternalDest }
| { type: "external"; external: SmartExternalDest };
export type SmartInternalDest = keyof RouteNamedMap;
export type SmartInternalDest =
| { route: keyof RouteNamedMap; id?: string }
| { route?: keyof RouteNamedMap; id: string };
export type SmartExternalDest = `https://${string}` | `http://${string}`;