Skip to content

Commit

Permalink
feat(post tags): add action menu
Browse files Browse the repository at this point in the history
  • Loading branch information
AlejandroAkbal committed Feb 29, 2024
1 parent 0c998c0 commit f6caf7a
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 94 deletions.
77 changes: 21 additions & 56 deletions components/pages/posts/post/Post.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
<script lang="ts" setup>
import {useUserSettings} from '~/composables/useUserSettings'
import {ChevronDownIcon} from '@heroicons/vue/24/outline'
import { useUserSettings } from '~/composables/useUserSettings'
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
import Tag from '~/assets/js/tag.dto'
import type { IPost } from '~/assets/js/post'
import { useAppStatistics } from '~/composables/useAppStatistics'
import {vOnLongPress} from '@vueuse/components'
import Tag from '~/assets/js/tag.dto'
import type {IPost} from '~/assets/js/post'
import {toast} from 'vue-sonner'
import {useAppStatistics} from '~/composables/useAppStatistics'
const props = defineProps<{
const props = defineProps<{
domain: string
post: IPost
selectedTags: Tag[]
}>()
// TODO: Find a better way to bubble events up
/**
* Events from child components
* @see PostTag.vue
*/
const emit = defineEmits<{
clickTag: [tag: string]
clickLongTag: [tag: string]
clickMiddleTag: [tag: string]
addTag: [tag: string]
setTag: [tag: string]
openTagInNewTab: [tag: string]
}>()
const userSettings = useUserSettings()
Expand Down Expand Up @@ -85,27 +87,6 @@ const props = defineProps<{
return tags
})
function onClickTag(tag: Tag) {
emit('clickTag', tag.name)
if (!tutorialLongClickTag.value) {
toast.info('Browsing Tip', {
description: 'Long click a tag to exclude it from search results',
duration: 10000
})
tutorialLongClickTag.value = true
}
}
function onClickLongTag(tag: Tag) {
emit('clickLongTag', tag.name)
}
function onClickMiddleTag(tag: Tag) {
emit('clickMiddleTag', tag.name)
}
</script>

<template>
Expand Down Expand Up @@ -167,29 +148,13 @@ const props = defineProps<{
v-for="tag in tagsAsSingleArray"
:key="tag.name"
>
<button
v-on-long-press.prevent.stop="() => onClickLongTag(tag)"
:class="{
'bg-primary-400/20 text-primary-400/90 ring-accent-400/20 hover:bg-primary-400/20': tag.type === 'artist',
'bg-green-400/20 text-green-400/90 ring-green-400/20 hover:bg-green-400/20': tag.type === 'copyright',
'bg-emerald-400/20 text-emerald-400/90 ring-emerald-400/20 hover:bg-emerald-400/20':
tag.type === 'character',
'hover:hover-bg-util': tag.type === 'general' || tag.type === 'meta',
// Mark tag as selected
'hover-bg-util hover-text-util !ring-base-0/20': selectedTags.some(
(selectedTag) => selectedTag.name === tag.name
)
}"
class="focus-visible:focus-outline-util group inline-flex select-none items-center rounded-full px-2 py-1 ring-1 ring-inset ring-base-0/20"
type="button"
@click="onClickTag(tag)"
@click.middle="onClickMiddleTag(tag)"
>
<span class="group-hover:hover-text-util text-xs font-medium">
{{ tag.name }}
</span>
</button>
<PostTag
:selectedTags="selectedTags"
:tag="tag"
@addTag="emit('addTag', $event)"
@openTagInNewTab="emit('openTagInNewTab', $event)"
@setTag="emit('setTag', $event)"
/>
</li>
</HeadlessDisclosurePanel>
</HeadlessDisclosure>
Expand Down
135 changes: 135 additions & 0 deletions components/pages/posts/post/PostTag.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<script lang="ts" setup>
import {
ArrowTopRightOnSquareIcon,
MagnifyingGlassIcon,
MinusIcon,
NoSymbolIcon,
PlusIcon
} from '@heroicons/vue/24/outline'
import type Tag from '~/assets/js/tag.dto'
const props = defineProps<{
tag: Tag
selectedTags: Tag[]
}>()
const emit = defineEmits<{
addTag: [tag: string]
setTag: [tag: string]
openTagInNewTab: [tag: string]
}>()
function isTagInSelectedTags(tag: Tag): boolean {
return props.selectedTags.some((selectedTag) => selectedTag.name === tag.name)
}
</script>

<template>
<HeadlessMenu
as="div"
class="relative inline-block text-left"
>
<!-- TODO: Fix placement to be auto -->
<Float
:offset="6"
enter="transition ease-out duration-100"
enter-from="transform opacity-0 scale-95"
enter-to="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leave-from="transform opacity-100 scale-100"
leave-to="transform opacity-0 scale-95"
placement="bottom-start"
portal
tailwindcss-origin-class
>
<HeadlessMenuButton
:class="{
'bg-primary-400/20 text-primary-400/90 ring-accent-400/20 hover:bg-primary-400/20': tag.type === 'artist',
'bg-green-400/20 text-green-400/90 ring-green-400/20 hover:bg-green-400/20': tag.type === 'copyright',
'bg-emerald-400/20 text-emerald-400/90 ring-emerald-400/20 hover:bg-emerald-400/20': tag.type === 'character',
'hover:hover-bg-util': tag.type === 'general' || tag.type === 'meta',
// Mark tag as selected
'hover-bg-util hover-text-util !ring-base-0/20': selectedTags.some(
(selectedTag) => selectedTag.name === tag.name
)
}"
class="focus-visible:focus-outline-util hover:hover-text-util select-none items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset ring-base-0/20"
>
{{ tag.name }}
</HeadlessMenuButton>

<HeadlessMenuItems
class="w-40 divide-y divide-base-0/20 rounded-md bg-base-1000 ring-1 ring-base-0/20 focus:outline-none"
>
<!-- Add or Remove tag -->
<div class="py-1">
<HeadlessMenuItem v-slot="{ active }">
<button
:class="[active ? 'bg-base-0/20 text-base-content-highlight' : 'text-base-content']"
class="group flex w-full items-center px-2.5 py-1 text-sm"
type="button"
@click="emit('addTag', props.tag.name)"
>
<component
:is="isTagInSelectedTags(tag) ? MinusIcon : PlusIcon"
class="mr-3 h-4 w-4 flex-shrink-0 rounded"
/>

{{ isTagInSelectedTags(tag) ? 'Remove tag' : 'Add tag' }}
</button>
</HeadlessMenuItem>
</div>

<!-- Exclude tag -->
<div class="py-1">
<HeadlessMenuItem v-slot="{ active }">
<button
:class="[active ? 'bg-base-0/20 text-base-content-highlight' : 'text-base-content']"
class="group flex w-full items-center px-2.5 py-1 text-sm"
type="button"
@click="emit('addTag', '-' + props.tag.name)"
>
<NoSymbolIcon class="mr-3 h-4 w-4 flex-shrink-0 rounded" />

Exclude tag
</button>
</HeadlessMenuItem>
</div>

<!-- Set tag -->
<div class="py-1">
<HeadlessMenuItem v-slot="{ active }">
<button
:class="[active ? 'bg-base-0/20 text-base-content-highlight' : 'text-base-content']"
class="group flex w-full items-center px-2.5 py-1 text-sm"
type="button"
@click="emit('setTag', props.tag.name)"
>
<MagnifyingGlassIcon class="mr-3 h-4 w-4 flex-shrink-0 rounded" />

Set tag
</button>
</HeadlessMenuItem>
</div>

<!-- Open in new tab -->
<div class="py-1">
<HeadlessMenuItem v-slot="{ active }">
<button
:class="[active ? 'bg-base-0/20 text-base-content-highlight' : 'text-base-content']"
class="group flex w-full items-center px-2.5 py-1 text-sm"
type="button"
@click="emit('openTagInNewTab', props.tag.name)"
>
<ArrowTopRightOnSquareIcon class="mr-3 h-4 w-4 flex-shrink-0 rounded" />

Open in new tab
</button>
</HeadlessMenuItem>
</div>
</HeadlessMenuItems>
</Float>
</HeadlessMenu>
</template>
81 changes: 43 additions & 38 deletions pages/posts/[domain].vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
<script lang="ts" setup>
import {useBooruList} from '~/composables/useBooruList'
import {ArrowPathIcon, ExclamationCircleIcon, QuestionMarkCircleIcon} from '@heroicons/vue/24/solid'
import {MagnifyingGlassIcon} from '@heroicons/vue/24/outline'
import {toast} from 'vue-sonner'
import Tag from '~/assets/js/tag.dto'
import type {Ref} from 'vue'
import {generatePostsRoute} from '~/assets/js/RouterHelper'
import {tagArrayToTitle} from '~/assets/js/SeoHelper'
import {capitalize} from 'lodash-es'
import type {Domain} from '~/assets/js/domain'
import type {IPostPage} from '~/assets/js/post'
import {useInfiniteQuery} from '@tanstack/vue-query'
import {FetchError} from 'ofetch'
import * as Sentry from '@sentry/vue'
const router = useRouter()
import { useBooruList } from '~/composables/useBooruList'
import { ArrowPathIcon, ExclamationCircleIcon, QuestionMarkCircleIcon } from '@heroicons/vue/24/solid'
import { MagnifyingGlassIcon } from '@heroicons/vue/24/outline'
import { toast } from 'vue-sonner'
import Tag from '~/assets/js/tag.dto'
import type { Ref } from 'vue'
import { generatePostsRoute } from '~/assets/js/RouterHelper'
import { tagArrayToTitle } from '~/assets/js/SeoHelper'
import { capitalize, cloneDeep } from 'lodash-es'
import type { Domain } from '~/assets/js/domain'
import type { IPostPage } from '~/assets/js/post'
import { useInfiniteQuery } from '@tanstack/vue-query'
import { FetchError } from 'ofetch'
import * as Sentry from '@sentry/vue'
const router = useRouter()
const route = useRoute()
const config = useRuntimeConfig()
const $authState = useState('auth-internal')
Expand Down Expand Up @@ -150,15 +150,14 @@ const router = useRouter()
queryKey: ['posts', selectedBooru, selectedTags, selectedFilters],
queryFn: fetchPosts,
select: (data) => {
// Delete all posts that have `media_type: 'unknown'`
data.pages.forEach((page) => {
page.data = page.data.filter((post) => post.media_type !== 'unknown')
})
return {
pages: data.pages,
pageParams: data.pageParams,
pageParams: data.pageParams
}
},
initialPageParam: '',
Expand Down Expand Up @@ -288,39 +287,45 @@ const router = useRouter()
/**
* Adds the tag, or removes it if it already exists
*/
async function onPostClickTag(tag: string) {
let newTags = undefined
async function onPostAddTag(tag: string) {
const isTagNegative = tag.startsWith('-')
let newTags = cloneDeep(selectedTags.value)
// Remove tag if it already exists
const isTagAlreadySelected = newTags.some((selectedTag) => selectedTag.name === tag)
const filteredSelectedTags = selectedTags.value.filter((selectedTag) => selectedTag.name !== tag)
if (isTagAlreadySelected) {
newTags = newTags.filter((selectedTag) => selectedTag.name !== tag)
// If the tag was not found, add it
if (filteredSelectedTags.length === selectedTags.value.length) {
newTags = [...selectedTags.value, new Tag({ name: tag })]
await reflectChangesInUrl({ page: null, tags: newTags })
return
}
// If the tag was found, remove it
else {
newTags = filteredSelectedTags
if (isTagNegative) {
const doesTagExistInPositive = newTags.some((selectedTag) => selectedTag.name === tag.slice(1))
if (doesTagExistInPositive) {
newTags = newTags.filter((selectedTag) => selectedTag.name !== tag.slice(1))
}
}
newTags.push(new Tag({ name: tag }))
await reflectChangesInUrl({ page: null, tags: newTags })
}
/**
* Removes the tag, and adds it to the blocklist
* Sets tags to only the given tag
*/
async function onPostClickLongTag(tag: string) {
const newTags = selectedTags.value.filter((selectedTag) => selectedTag.name !== tag)
newTags.push(new Tag({ name: '-' + tag }))
await reflectChangesInUrl({ page: null, tags: newTags })
async function onPostSetTag(tag: string) {
await reflectChangesInUrl({ page: null, tags: [new Tag({ name: tag })] })
}
/**
* Opens the tag in a new tab
*/
async function onPostClickMiddleTag(tag: string) {
async function onPostOpenTagInNewTab(tag: string) {
const tagUrl = generatePostsRoute(
undefined,
selectedBooru.value.domain,
Expand Down Expand Up @@ -649,9 +654,9 @@ const router = useRouter()
:domain="selectedBooru.domain"
:post="post"
:selected-tags="selectedTags"
@click-tag="onPostClickTag"
@click-long-tag="onPostClickLongTag"
@click-middle-tag="onPostClickMiddleTag"
@addTag="onPostAddTag"
@openTagInNewTab="onPostOpenTagInNewTab"
@setTag="onPostSetTag"
/>
</li>

Expand Down

0 comments on commit f6caf7a

Please sign in to comment.