diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index a025d84b6599988d21596e4ec1fd8110e3b27370..2787092ac713d7d89090ee1fd03c4cb00b469d45 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -24,6 +24,7 @@ export default defineConfig({ { text: "Radio Card", link: "/components/card/radio" }, { text: "Artist Card", link: "/components/card/artist" }, { text: "Album Card", link: "/components/card/album" }, + { text: "Playlist Card", link: "/components/card/playlist" }, ] }, ] } diff --git a/docs/components/card/playlist.md b/docs/components/card/playlist.md new file mode 100644 index 0000000000000000000000000000000000000000..5bc64d5da9b42a105d2b78ab0ee312a465fc4644 --- /dev/null +++ b/docs/components/card/playlist.md @@ -0,0 +1,25 @@ +<script setup lang="ts"> +const playlist = { + name: 'Incredible Playlist', + user: { + full_username: '@username:example.com', + username: '@username' + }, + tracks_count: 27, + album_covers: [ + 'https://images.unsplash.com/photo-1524650359799-842906ca1c06?ixlib=rb-1.2.1&dl=te-nguyen-Wt7XT1R6sjU-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb', + 'https://unsplash.com/photos/SVGan4GCopM/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTV8fG11c2ljaWFufGVufDB8Mnx8fDE2NjI5NzQwNjI&force=true&w=640', + 'https://unsplash.com/photos/P4gXe-RsEXI/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8Nnx8cGVyZm9ybWVyfGVufDB8Mnx8fDE2NjMwMTM3OTE&force=true&w=640' + ] +} +</script> + +# Playlist card +## Normal card +```html +<fw-playlist-card :playlist="playlist" /> +``` +<div style="display: grid; grid-template-columns: auto auto 1fr; grid-gap: 2rem;"> +<fw-playlist-card :playlist="playlist" /> +<fw-playlist-card :playlist="{ ...playlist, album_covers: [] }" /> +</div> diff --git a/src/components/card/Card.vue b/src/components/card/Card.vue index 98ca5a9be4c6aa92dff6cc77e1720d0fa2734d7b..7f1306bb288069681eed0cd3d49794a444d69001 100644 --- a/src/components/card/Card.vue +++ b/src/components/card/Card.vue @@ -16,13 +16,21 @@ defineProps<Props>() <div @click="(event) => !buttonTitle ? onClick?.(event) : undefined" class="funkwhale card" - :class="{ 'has-image': !!image, 'is-link': !!onClick, 'is-category': category, 'is-cta': !!buttonTitle }" + :class="{ 'has-image': !!image || $slots.image, 'is-link': !!onClick, 'is-category': category, 'is-cta': !!buttonTitle }" > + <div + v-if="$slots.image" + class="card-image" + > + <slot name="image" :src="image" /> + </div> + <img - v-if="image" + v-else-if="image" :src="image" class="card-image" /> + <div class="card-title">{{ title }}</div> <div v-if="$slots.default" diff --git a/src/components/card/playlist/Card.vue b/src/components/card/playlist/Card.vue new file mode 100644 index 0000000000000000000000000000000000000000..48f7f10e876b740faa4519eade088bcd9ab6217f --- /dev/null +++ b/src/components/card/playlist/Card.vue @@ -0,0 +1,87 @@ +<script setup lang="ts"> +import { FwCard, FwPlayButton, FwOptionsButton } from '~/components' +import { useRouter } from 'vue-router' +import { useI18n } from 'vue-i18n' +import { computed } from 'vue' + +const { t } = useI18n() + +interface Playlist { + id: number + name: string + user: { + full_username: string + username: string + } + tracks_count: number + album_covers: string[] +} + +interface Events { + (e: 'play', playlist: Playlist): void +} + +interface Props { + playlist: Playlist +} + +const emit = defineEmits<Events>() +const props = defineProps<Props>() + +const covers = computed(() => props.playlist.album_covers + .filter((src, index, array) => array.indexOf(src) === index) + .slice(0, 4) +) + +const profileParams = computed(() => { + const [username, domain] = props.playlist.user.full_username.split('@') + return { username, domain } +}) + +let navigate = (to: 'playlist' | 'user') => {} + +if (import.meta.env.PROD) { + const router = useRouter() + navigate = (to: 'playlist' | 'user') => to === 'playlist' + ? router.push({ name: 'library.playlists.detail', params: { id: props.playlist.id } }) + : router.push({ name: 'profile.full', params: profileParams.value }) +} +</script> + +<template> + <fw-card + :title="playlist.name" + @click="navigate('playlist')" + class="playlist-card" + > + <template #image> + <img + v-for="src in covers" + :key="src" + :src="src" + /> + <div + v-for="i in Math.max(0, 4 - covers.length)" + :key="i" + /> + </template> + + <fw-play-button @play="emit('play', playlist)" /> + + <a + @click.stop="navigate('user')" + class="funkwhale link artist-link" + > + {{ t('vui.playlist-by', playlist.user) }} + </a> + + <div class="card-footer"> + {{ t('vui.tracks', playlist.tracks_count) }} + <fw-options-button /> + </div> + </fw-card> +</template> + +<style lang="scss"> +@import './style.scss' +</style> diff --git a/src/components/card/playlist/style.scss b/src/components/card/playlist/style.scss new file mode 100644 index 0000000000000000000000000000000000000000..7c59cad0f372d2bc43b04ebacaf6c5b4a4f38197 --- /dev/null +++ b/src/components/card/playlist/style.scss @@ -0,0 +1,91 @@ +.funkwhale { + &.card.playlist-card { + --fw-card-width: 208px; + + position: relative; + width: var(--fw-card-width); + padding-bottom: 14px; + + &:hover { + .play-button { + opacity: 1 !important; + transform: translateX(6px) !important; + } + + .options-button { + opacity: 1 !important; + transform: translate(12px) !important; + } + } + + .play-button, + .options-button { + opacity: 0; + } + + .options-button { + transform: translateX(6px) !important; + transition-delay: .1s; + } + + --fw-image-width: var(--fw-card-width); + > .card-image { + border-radius: 0 !important; + height: var(--fw-image-width); + width: var(--fw-image-width); + max-width: var(--fw-image-width); + margin: -24px -24px 0; + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + + > div { + --fw-bg-color: var(--fw-pastel-blue-3); + background-color: var(--fw-bg-color); + &:nth-child(2) { + --fw-bg-color: var(--fw-pastel-blue-1); + } + &:nth-child(3) { + --fw-bg-color: var(--fw-pastel-blue-2); + } + &:nth-child(4) { + --fw-bg-color: var(--fw-pastel-blue-4); + } + } + } + + > .card-title { + font-size: 1rem; + text-align: left !important; + padding-top: 16px !important; + } + + > .card-content { + padding-top: 0 !important; + text-align: left !important; + + > .artist-link { + font-size: 0.875rem; + --fw-link-color: var(--fw-text-color); + } + + > .play-button { + position: absolute; + right: 8px; + top: calc(var(--fw-image-width) - 8px - 44px); + } + + > .card-footer { + display: flex; + align-items: center; + color: var(--fw-grey-700); + font-size: 0.8125rem; + padding-top: 6px; + + > .options-button { + margin-left: auto; + } + } + } + } +} diff --git a/src/components/card/style.scss b/src/components/card/style.scss index fbeb82025d57a18f6edb31fb06b07bcb9c89a08c..5796a20471d0b868ba9b8b69471a91d960960d5b 100644 --- a/src/components/card/style.scss +++ b/src/components/card/style.scss @@ -12,7 +12,8 @@ cursor: pointer; } - > img.card-image { + > .card-image { + overflow: hidden; border-radius: var(--fw-border-radius); object-fit: cover; height: 160px; diff --git a/src/components/index.ts b/src/components/index.ts index cf0bcd1f4ca61d61fc9bb2bec16dbe63a0a812c1..8504a645cb2bc40ecee82aa193fd04c4e73c0902 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,11 +1,17 @@ +// Buttons export { default as FwOptionsButton } from './button/options/Button.vue' export { default as FwPlayButton } from './button/play/Button.vue' export { default as FwButton } from './button/Button.vue' +// Cards +export { default as FwPlaylistCard } from './card/playlist/Card.vue' export { default as FwArtistCard } from './card/artist/Card.vue' export { default as FwAlbumCard } from './card/album/Card.vue' export { default as FwRadioCard } from './card/radio/Card.vue' export { default as FwCard } from './card/Card.vue' -export { default as FwLoader } from './loader/Loader.vue' +// Pills export { default as FwPill } from './pill/Pill.vue' + +// Loader +export { default as FwLoader } from './loader/Loader.vue' diff --git a/src/locales/en.yaml b/src/locales/en.yaml index 4e46204a9ad5dc1032e729282e5018f2e612e479..84ae584ce3512b181a2d76582ac0d5c255f6b76b 100644 --- a/src/locales/en.yaml +++ b/src/locales/en.yaml @@ -2,3 +2,4 @@ vui: radio: Radio albums: '{n} album | {n} albums' tracks: '{n} track | {n} tracks' + playlist-by: 'by {username}' diff --git a/src/styles/base/generic.scss b/src/styles/base/generic.scss index 4948b6b615d7d69fb06e157af35f728743034a28..0102dfd3ef7767f3135710dcac01b0568195fe05 100644 --- a/src/styles/base/generic.scss +++ b/src/styles/base/generic.scss @@ -3,7 +3,7 @@ a { --fw-link-color: var(--fw-blue-500); - &:not(.VPLink):not(.vp-doc .header-anchor):not(.VPDocAsideOutline .outline-link):not(.VPNavBarTitle .title) { + &:not(.VPLink):not(.vp-doc .header-anchor):not(.VPDocAsideOutline .outline-link):not(.VPNavBarTitle .title):not(.VPDocFooter .pager-link) { color: var(--fw-link-color); &:hover,