feat: add formidable ESLint + Prettier linting setup
Some checks failed
Build and Push Backend Image / build (push) Successful in 47s
Build and Push Frontend Image / build (push) Has been cancelled

- Root-level eslint.config.js (flat config): typescript-eslint,
  eslint-plugin-svelte, eslint-config-prettier, @eslint/js
- Root-level prettier.config.js with prettier-plugin-svelte
- svelte-check added to frontend for Svelte/TS type checking
- lint, lint:fix, format, format:check, check scripts in root
  and both packages
- All 60 lint errors fixed across backend and frontend:
  - Consistent type imports
  - Removed unused imports/variables
  - Added keys to all {#each} blocks for Svelte performance
  - Replaced mutable Set/Map with SvelteSet/SvelteMap
  - Fixed useless assignments and empty catch blocks
- 64 remaining warnings are intentional any usages in the
  Pothos/Drizzle GraphQL resolver layer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 22:24:55 +01:00
parent 741e0c3387
commit 18116072c9
43 changed files with 1023 additions and 5048 deletions

View File

@@ -76,7 +76,7 @@ const { data } = $props();
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-3xl mx-auto">
{#each data.models as model}
{#each data.models as model (model.slug)}
<Card
class="p-0 group hover:shadow-2xl hover:shadow-primary/20 transition-all duration-300 hover:-translate-y-2 bg-gradient-to-br from-card to-card/50 border-primary/20"
>
@@ -128,7 +128,7 @@ const { data } = $props();
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-6xl mx-auto">
{#each data.videos as video}
{#each data.videos as video (video.slug)}
<Card
class="p-0 group hover:shadow-2xl hover:shadow-accent/20 transition-all duration-300 hover:-translate-y-2 bg-gradient-to-br from-card to-card/50 border-accent/20 overflow-hidden"
>

View File

@@ -108,7 +108,7 @@ const values = [
<section class="py-16 bg-card/30">
<div class="container mx-auto px-4">
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
{#each stats as stat}
{#each stats as stat (stat.icon)}
<div class="text-center">
<div
class="w-16 h-16 bg-gradient-to-br from-primary/20 to-accent/20 rounded-full flex items-center justify-center mx-auto mb-4"
@@ -176,7 +176,7 @@ const values = [
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
{#each values as value}
{#each values as value (value.title)}
<Card
class="bg-gradient-to-br from-card to-card/50 border-primary/20 hover:shadow-xl hover:shadow-primary/10 transition-all duration-300"
>
@@ -214,7 +214,7 @@ const values = [
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
{#each team as member}
{#each team as member (member.name)}
<Card
class="bg-gradient-to-br from-card to-card/50 border-primary/20 hover:shadow-xl hover:shadow-primary/10 transition-all duration-300 hover:-translate-y-2"
>

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { _ } from "svelte-i18n";
import { SvelteSet } from "svelte/reactivity";
import {
Card,
CardContent,
@@ -12,7 +13,7 @@ import PeonyBackground from "$lib/components/background/peony-background.svelte"
import Meta from "$lib/components/meta/meta.svelte";
let searchQuery = $state("");
let expandedItems = $state<Set<number>>(new Set());
let expandedItems = new SvelteSet<number>();
const faqCategories = [
{
@@ -171,7 +172,7 @@ const filteredQuestions = $derived(() => {
});
function toggleExpanded(id: number) {
const newExpanded = new Set(expandedItems);
const newExpanded = new SvelteSet(expandedItems);
if (newExpanded.has(id)) {
newExpanded.delete(id);
} else {
@@ -224,7 +225,7 @@ function toggleExpanded(id: number) {
})}
</h2>
<div class="space-y-4">
{#each filteredQuestions() as question}
{#each filteredQuestions() as question (question.id)}
<Card
class="bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10"
>
@@ -273,7 +274,7 @@ function toggleExpanded(id: number) {
<!-- Category View -->
<div class="max-w-6xl mx-auto">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
{#each faqCategories as category}
{#each faqCategories as category (category.id)}
<Card
class="bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10"
>
@@ -290,7 +291,7 @@ function toggleExpanded(id: number) {
</CardHeader>
<CardContent class="pt-0">
<div class="space-y-3">
{#each category.questions as question}
{#each category.questions as question (question.id)}
<div
class="border border-border/50 rounded-lg overflow-hidden"
>

View File

@@ -10,8 +10,6 @@ import {
} from "$lib/components/ui/card";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Checkbox } from "$lib/components/ui/checkbox";
import PeonyIcon from "$lib/components/icon/peony-icon.svelte";
import * as Alert from "$lib/components/ui/alert";
import { goto } from "$app/navigation";
import { login } from "$lib/services";
@@ -24,7 +22,7 @@ let email = $state("");
let password = $state("");
let error = $state("");
let showPassword = $state(false);
let rememberMe = $state(false);
let _rememberMe = $state(false);
let isLoading = $state(false);
let isError = $state(false);
@@ -212,7 +210,7 @@ onMount(() => {
<!-- Sign Up Link -->
<div class="text-center">
<p class="text-sm text-muted-foreground">
{$_("auth.login.no_account")}{" "}
{$_("auth.login.no_account")}
<a href="/signup" class="text-primary hover:underline font-medium"
>{$_("auth.login.sign_up_link")}</a
>

View File

@@ -261,7 +261,7 @@ const filteredArticles = $derived(() => {
<!-- Articles Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{#each filteredArticles() as article}
{#each filteredArticles() as article (article.slug)}
<Card
class="p-0 group hover:shadow-2xl hover:shadow-primary/25 transition-all duration-300 hover:-translate-y-3 bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10 overflow-hidden"
>
@@ -316,7 +316,7 @@ const filteredArticles = $derived(() => {
<!-- Tags -->
<div class="flex flex-wrap gap-2 mb-4">
{#each article.tags.slice(0, 3) as tag}
{#each article.tags.slice(0, 3) as tag (tag)}
<a
class="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"
href="/tags/{tag}"

View File

@@ -6,7 +6,6 @@ import { Card, CardContent } from "$lib/components/ui/card";
import { calcReadingTime } from "$lib/utils";
import TimeAgo from "javascript-time-ago";
import { getAssetUrl } from "$lib/directus";
import SharingPopup from "$lib/components/sharing-popup/sharing-popup.svelte";
import Meta from "$lib/components/meta/meta.svelte";
import PeonyBackground from "$lib/components/background/peony-background.svelte";
import SharingPopupButton from "$lib/components/sharing-popup/sharing-popup-button.svelte";
@@ -129,7 +128,7 @@ const timeAgo = new TimeAgo("en");
<span class="font-semibold">Tags</span>
</div>
<div class="flex flex-wrap gap-2">
{#each data.article.tags as tag}
{#each data.article.tags as tag (tag)}
<a class="bg-primary/10 text-primary px-3 py-1 rounded-full text-sm" href="/tags/{tag}">
#{tag}
</a>

View File

@@ -31,7 +31,6 @@ import {
FileDropZone,
MEGABYTE,
} from "$lib/components/ui/file-drop-zone";
import * as Avatar from "$lib/components/ui/avatar";
import RecordingCard from "$lib/components/recording-card/recording-card.svelte";
const { data } = $props();
@@ -175,7 +174,7 @@ async function handleDeleteRecording(id: string) {
await deleteRecording(id);
recordings = recordings.filter((r) => r.id !== id);
toast.success($_("me.recordings.delete_success"));
} catch (error) {
} catch {
toast.error($_("me.recordings.delete_error"));
}
}
@@ -625,7 +624,7 @@ onMount(() => {
</tr>
</thead>
<tbody>
{#each data.analytics.videos as video}
{#each data.analytics.videos as video (video.slug)}
<tr class="border-b border-border/50 hover:bg-primary/5 transition-colors">
<td class="p-3">
<a href="/videos/{video.slug}" class="hover:text-primary transition-colors">

View File

@@ -144,7 +144,7 @@ const filteredModels = $derived(() => {
<!-- Models Grid -->
<div class="container mx-auto px-4 py-12">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{#each filteredModels() as model}
{#each filteredModels() as model (model.slug)}
<Card
class="py-0 group hover:shadow-2xl hover:shadow-primary/25 transition-all duration-500 hover:-translate-y-3 bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10 overflow-hidden"
>
@@ -211,7 +211,7 @@ const filteredModels = $derived(() => {
<!-- Tags -->
<div class="flex flex-wrap gap-2 mb-4">
{#each model.tags as tag}
{#each model.tags as tag (tag)}
<a
class="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"
href="/tags/{tag}"

View File

@@ -131,7 +131,7 @@ let totalPlays = $derived(
<!-- Tags -->
<div class="flex flex-wrap gap-2">
{#each data.model.tags as tag}
{#each data.model.tags as tag (tag)}
<a
class="text-xs bg-primary/10 text-primary px-3 py-1 rounded-full"
href="/tags/{tag}"
@@ -227,7 +227,7 @@ let totalPlays = $derived(
<TabsContent value="videos">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each data.videos as video}
{#each data.videos as video (video.slug)}
<Card
class="p-0 group hover:shadow-2xl hover:shadow-primary/20 transition-all duration-300 hover:-translate-y-2 bg-gradient-to-br from-card to-card/50 border-primary/20 overflow-hidden"
>

View File

@@ -10,10 +10,9 @@ import {
} from "$lib/components/ui/card";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import PeonyIcon from "$lib/components/icon/peony-icon.svelte";
import * as Alert from "$lib/components/ui/alert";
import { goto } from "$app/navigation";
import { login, requestPassword } from "$lib/services";
import { requestPassword } from "$lib/services";
import { onMount } from "svelte";
import PeonyBackground from "$lib/components/background/peony-background.svelte";
import { toast } from "svelte-sonner";

View File

@@ -10,7 +10,6 @@ import {
} from "$lib/components/ui/card";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import PeonyIcon from "$lib/components/icon/peony-icon.svelte";
import * as Alert from "$lib/components/ui/alert";
import { goto } from "$app/navigation";
import { resetPassword } from "$lib/services";

View File

@@ -4,8 +4,8 @@ import Meta from "$lib/components/meta/meta.svelte";
import {
ButtplugClient,
ButtplugWasmClientConnector,
ButtplugClientDevice,
OutputType,
type ButtplugClientDevice,
type OutputType,
InputType,
DeviceOutputValueConstructor,
} from "@sexy.pivoine.art/buttplug";
@@ -149,7 +149,7 @@ async function handleStop(device: BluetoothDevice) {
}
function convertDevice(device: ButtplugClientDevice): BluetoothDevice {
const actuators: import("$lib/types").DeviceActuator[] = [];
const actuators: import("$lib/types").DeviceActuator[] = []; // eslint-disable-line @typescript-eslint/consistent-type-imports
for (const [, feature] of device.features) {
for (const outputType of feature.outputTypes) {
actuators.push({
@@ -568,7 +568,7 @@ onMount(() => {
<div class="container mx-auto px-4 py-12">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{#if devices}
{#each devices as device}
{#each devices as device (device.name)}
<DeviceCard
{device}
onChange={(scalarIndex, val) => handleChange(device, scalarIndex, val)}

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { _ } from "svelte-i18n";
import { SvelteMap } from "svelte/reactivity";
import * as Dialog from "$lib/components/ui/dialog";
import Button from "$lib/components/ui/button/button.svelte";
import type { BluetoothDevice, DeviceInfo } from "$lib/types";
@@ -15,7 +16,7 @@ interface Props {
let { open, recordedDevices, connectedDevices, onConfirm, onCancel }: Props = $props();
// Device mappings: recorded device name -> connected device
let mappings = $state<Map<string, BluetoothDevice>>(new Map());
let mappings = new SvelteMap<string, BluetoothDevice>();
// Check if a connected device is compatible with a recorded device
function isCompatible(recordedDevice: DeviceInfo, connectedDevice: BluetoothDevice): boolean {
@@ -37,7 +38,7 @@ function getCompatibleDevices(recordedDevice: DeviceInfo): BluetoothDevice[] {
// Auto-map devices on open
$effect(() => {
if (open && recordedDevices.length > 0 && connectedDevices.length > 0) {
const newMappings = new Map<string, BluetoothDevice>();
const newMappings = new SvelteMap<string, BluetoothDevice>();
recordedDevices.forEach(recordedDevice => {
// Try to find exact name match first
@@ -74,7 +75,7 @@ function handleDeviceSelect(recordedDeviceName: string, deviceId: string) {
const device = connectedDevices.find(d => d.id === deviceId);
if (device) {
const newMappings = new Map(mappings);
const newMappings = new SvelteMap(mappings);
newMappings.set(recordedDeviceName, device);
mappings = newMappings;
}
@@ -95,7 +96,7 @@ const allDevicesMapped = $derived(
</Dialog.Header>
<div class="space-y-4 py-4">
{#each recordedDevices as recordedDevice}
{#each recordedDevices as recordedDevice (recordedDevice.name)}
{@const compatibleDevices = getCompatibleDevices(recordedDevice)}
{@const currentMapping = mappings.get(recordedDevice.name)}
@@ -106,7 +107,7 @@ const allDevicesMapped = $derived(
<h3 class="font-semibold text-card-foreground">{recordedDevice.name}</h3>
</div>
<div class="flex flex-wrap gap-1">
{#each recordedDevice.capabilities as capability}
{#each recordedDevice.capabilities as capability (capability)}
<span class="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20">
{capability}
</span>
@@ -129,7 +130,7 @@ const allDevicesMapped = $derived(
onchange={(e) => handleDeviceSelect(recordedDevice.name, e.currentTarget.value)}
>
<option value="" disabled>Select device...</option>
{#each compatibleDevices as device}
{#each compatibleDevices as device (device.name)}
<option value={device.id}>
{device.name}
{#if device.name === recordedDevice.name}(exact match){/if}

View File

@@ -96,7 +96,7 @@ function handleCancel() {
<!-- Device Info -->
<div class="space-y-2">
<Label>Devices Used</Label>
{#each deviceInfo as device}
{#each deviceInfo as device (device.name)}
<div
class="flex items-center gap-2 text-sm bg-muted/20 rounded px-3 py-2"
>

View File

@@ -14,7 +14,6 @@ import { Label } from "$lib/components/ui/label";
import { Checkbox } from "$lib/components/ui/checkbox";
import { toast } from "svelte-sonner";
import * as Alert from "$lib/components/ui/alert";
import PeonyIcon from "$lib/components/icon/peony-icon.svelte";
import { register } from "$lib/services";
import PeonyBackground from "$lib/components/background/peony-background.svelte";
import Meta from "$lib/components/meta/meta.svelte";

View File

@@ -10,7 +10,6 @@ import {
SelectTrigger,
} from "$lib/components/ui/select";
import { getAssetUrl } from "$lib/directus";
import PeonyBackground from "$lib/components/background/peony-background.svelte";
import Meta from "$lib/components/meta/meta.svelte";
let searchQuery = $state("");
@@ -134,7 +133,7 @@ const filteredItems = $derived(() => {
<!-- Items Grid -->
<div class="container mx-auto px-4 py-12">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{#each filteredItems() as item}
{#each filteredItems() as item (item.slug)}
<Card
class="py-0 group hover:shadow-2xl hover:shadow-primary/25 transition-all duration-300 hover:-translate-y-3 bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10 overflow-hidden"
>
@@ -178,7 +177,7 @@ const filteredItems = $derived(() => {
<!-- Tags -->
<div class="flex flex-wrap gap-2 mb-4">
{#each item.tags as tag}
{#each item.tags as tag (tag)}
<a
class="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"
href="/tags/{tag}"

View File

@@ -200,7 +200,7 @@ const filteredVideos = $derived(() => {
<!-- Videos Grid -->
<div class="container mx-auto px-4 py-12">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{#each filteredVideos() as video}
{#each filteredVideos() as video (video.slug)}
<Card
class="p-0 group hover:shadow-2xl hover:shadow-primary/25 transition-all duration-500 hover:-translate-y-3 bg-gradient-to-br from-card/90 via-card/95 to-card/85 backdrop-blur-xl shadow-lg shadow-primary/10 overflow-hidden"
>

View File

@@ -33,7 +33,7 @@ let commentError = $state();
let currentPlayId = $state<string | null>(null);
let lastTrackedTime = $state(0);
const relatedVideos = [
const _relatedVideos = [
{
id: 2,
title: "Sunset Dreams",
@@ -94,7 +94,7 @@ async function handleLike() {
}
}
function handleBookmark() {
function _handleBookmark() {
isBookmarked = !isBookmarked;
}
@@ -279,7 +279,7 @@ let showPlayer = $state(false);
/>
<!-- <Button
variant={isBookmarked ? "default" : "outline"}
onclick={handleBookmark}
onclick={_handleBookmark}
class="flex items-center gap-2 {isBookmarked
? 'bg-gradient-to-r from-primary to-accent'
: 'border-primary/20 hover:bg-primary/10'}"
@@ -291,7 +291,7 @@ let showPlayer = $state(false);
<!-- Model Info -->
<div class="grid grid-cols-1 gap-4">
{#each data.video.models as model}
{#each data.video.models as model (model.slug)}
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<a href={`/models/${model.slug}`}>
@@ -341,7 +341,7 @@ let showPlayer = $state(false);
<CardContent class="p-4">
<p class="text-muted-foreground mb-4">{data.video.description}</p>
<div class="flex flex-wrap gap-2">
{#each data.video.tags as tag}
{#each data.video.tags as tag (tag)}
<a
class="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"
href="/tags/{tag}"
@@ -438,7 +438,7 @@ let showPlayer = $state(false);
{#if showComments}
<!-- Comments List -->
<div class="space-y-4">
{#each data.comments as comment}
{#each data.comments as comment (comment.id)}
<div class="flex gap-3">
<a href="/users/{comment.user_created.id}" class="flex-shrink-0">
<Avatar