mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-09 20:12:10 +10:00
* feat: small library tweaks + company page * feat: new store view * fix: ci merge error * feat: add genres to store page * feat: sorting * feat: lock game/version imports while their tasks are running * feat: feature games * feat: tag based filtering * fix: make tags alphabetical * refactor: move a bunch of i18n to common * feat: add localizations for everything * fix: title description on panel * fix: feature carousel text * fix: i18n footer strings * feat: add tag page * fix: develop merge * feat: offline games support (don't error out if provider throws) * feat: tag management * feat: show library next to game import + small fixes * feat: most of the company and tag managers * feat: company text field editing * fix: small fixes + tsgo experiemental * feat: upload icon and banner * feat: store infinite scrolling and bulk import mode * fix: lint * fix: add drop-base to prettier ignore
213 lines
6.4 KiB
Vue
213 lines
6.4 KiB
Vue
<!-- eslint-disable vue/no-v-html -->
|
|
<template>
|
|
<div
|
|
class="flex grow flex-col gap-y-5 overflow-y-auto bg-zinc-900 px-6 py-6 ring-1 ring-white/10"
|
|
>
|
|
<!-- Search and filters -->
|
|
<div class="space-y-6">
|
|
<div>
|
|
<label for="search" class="sr-only">{{ $t("news.search") }}</label>
|
|
<div class="relative">
|
|
<div
|
|
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
|
|
>
|
|
<MagnifyingGlassIcon
|
|
class="h-5 w-5 text-zinc-400"
|
|
aria-hidden="true"
|
|
/>
|
|
</div>
|
|
<input
|
|
id="search"
|
|
v-model="searchQuery"
|
|
type="text"
|
|
class="block w-full rounded-md border-0 bg-zinc-800 py-2.5 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
|
|
:placeholder="$t('news.searchPlaceholder')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pt-2">
|
|
<label
|
|
for="date"
|
|
class="block text-sm font-medium text-zinc-400 mb-2"
|
|
>{{ $t("common.date") }}</label
|
|
>
|
|
<select
|
|
id="date"
|
|
v-model="dateFilter"
|
|
class="mt-1 block w-full rounded-md border-0 bg-zinc-800 py-2 pl-3 pr-10 text-zinc-100 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
|
|
>
|
|
<option value="all">{{ $t("news.filter.all") }}</option>
|
|
<option value="today">{{ $t("common.today") }}</option>
|
|
<option value="week">{{ $t("news.filter.week") }}</option>
|
|
<option value="month">{{ $t("news.filter.month") }}</option>
|
|
<option value="year">{{ $t("news.filter.year") }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-zinc-400 mb-2">
|
|
{{ $t("common.tags") }}
|
|
</label>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button
|
|
v-for="tag in availableTags"
|
|
:key="tag"
|
|
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors duration-200"
|
|
:class="[
|
|
selectedTags.includes(tag)
|
|
? 'bg-blue-600 text-white'
|
|
: 'bg-zinc-800 text-zinc-300 hover:bg-zinc-700',
|
|
]"
|
|
@click="toggleTag(tag)"
|
|
>
|
|
{{ tag }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="flex-1 space-y-2">
|
|
<NuxtLink
|
|
v-for="article in filteredArticles"
|
|
:key="article.id"
|
|
:to="`/news/${article.id}`"
|
|
class="group block rounded-lg hover-lift"
|
|
>
|
|
<div
|
|
class="relative flex flex-col gap-y-2 rounded-lg p-3 transition-all duration-200"
|
|
:class="[
|
|
route.params.id === article.id
|
|
? 'bg-zinc-800'
|
|
: 'hover:bg-zinc-800/50',
|
|
]"
|
|
>
|
|
<div
|
|
v-if="article.imageObjectId"
|
|
class="absolute inset-0 rounded-lg transition-all duration-200 overflow-hidden"
|
|
>
|
|
<img
|
|
:src="useObject(article.imageObjectId)"
|
|
class="absolute blur-sm inset-0 w-full h-full object-cover transition-all duration-200 group-hover:scale-110"
|
|
/>
|
|
<div class="absolute inset-0 bg-zinc-900/50" />
|
|
</div>
|
|
|
|
<h3 class="relative text-sm font-medium text-zinc-100">
|
|
{{ article.title }}
|
|
</h3>
|
|
<p
|
|
class="relative mt-1 text-xs text-zinc-400 line-clamp-2"
|
|
v-html="formatExcerpt(article.description)"
|
|
/>
|
|
<div
|
|
class="relative mt-2 flex items-center gap-x-2 text-xs text-zinc-500"
|
|
>
|
|
<time :datetime="article.publishedAt">
|
|
{{ $d(new Date(article.publishedAt), "short") }}
|
|
</time>
|
|
</div>
|
|
</div>
|
|
</NuxtLink>
|
|
</nav>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from "vue";
|
|
import { MagnifyingGlassIcon } from "@heroicons/vue/24/solid";
|
|
import { micromark } from "micromark";
|
|
|
|
const news = useNews();
|
|
if (!news.value) {
|
|
news.value = await fetchNews();
|
|
}
|
|
|
|
const route = useRoute();
|
|
const searchQuery = ref("");
|
|
const dateFilter = ref("all");
|
|
const selectedTags = ref<string[]>([]);
|
|
|
|
// Get unique tags from all articles
|
|
const availableTags = computed(() => {
|
|
if (!news.value) return [];
|
|
const tags = new Set<string>();
|
|
news.value.forEach((article) => {
|
|
article.tags.forEach((tag) => tags.add(tag.name));
|
|
});
|
|
return Array.from(tags);
|
|
});
|
|
|
|
const toggleTag = (tag: string) => {
|
|
const index = selectedTags.value.indexOf(tag);
|
|
if (index === -1) {
|
|
selectedTags.value.push(tag);
|
|
} else {
|
|
selectedTags.value.splice(index, 1);
|
|
}
|
|
};
|
|
|
|
const formatExcerpt = (excerpt: string) => {
|
|
// Convert markdown to HTML, micromark is safe
|
|
return micromark(excerpt);
|
|
};
|
|
|
|
const filteredArticles = computed(() => {
|
|
if (!news.value) return [];
|
|
|
|
// filter articles based on search, date, and tags
|
|
return news.value.filter((article) => {
|
|
const matchesSearch =
|
|
article.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
|
article.description
|
|
.toLowerCase()
|
|
.includes(searchQuery.value.toLowerCase());
|
|
|
|
const articleDate = new Date(article.publishedAt);
|
|
const now = new Date();
|
|
let matchesDate = true;
|
|
|
|
switch (dateFilter.value.toLowerCase()) {
|
|
case "today": {
|
|
matchesDate = articleDate.toDateString() === now.toDateString();
|
|
break;
|
|
}
|
|
case "week": {
|
|
const weekAgo = new Date(now.setDate(now.getDate() - 7));
|
|
matchesDate = articleDate >= weekAgo;
|
|
break;
|
|
}
|
|
case "month": {
|
|
matchesDate =
|
|
articleDate.getMonth() === now.getMonth() &&
|
|
articleDate.getFullYear() === now.getFullYear();
|
|
break;
|
|
}
|
|
case "year": {
|
|
matchesDate = articleDate.getFullYear() === now.getFullYear();
|
|
break;
|
|
}
|
|
}
|
|
|
|
const matchesTags =
|
|
selectedTags.value.length === 0 ||
|
|
selectedTags.value.every((tag) =>
|
|
article.tags.find((e) => e.name == tag),
|
|
);
|
|
|
|
return matchesSearch && matchesDate && matchesTags;
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.hover-lift {
|
|
transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
}
|
|
|
|
.hover-lift:hover {
|
|
transform: translateY(-2px) scale(1.02);
|
|
}
|
|
</style>
|