feat. home 버튼 컴포넌트 추가

This commit is contained in:
clkim
2025-11-11 18:50:14 +09:00
parent 25ea7925ea
commit 4ca00e4028
20 changed files with 216 additions and 317 deletions

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
const scrollStore = useScrollStore()
const { isPassedStoveGnb } = storeToRefs(scrollStore)
</script>
<template>
<AtomsButtonCircle
sr-only="home"
type="link"
to="/"
:class="['btn-home', { 'is-fixed': isPassedStoveGnb }]"
>
<AtomsIconsHomeFill />
</AtomsButtonCircle>
</template>
<style scoped>
.btn-home {
@apply absolute top-3 right-3 mt-[48px] bg-black/20 shadow-[0_1.667px_3.333px_0_rgba(0,0,0,0.06)] backdrop-blur-[12.5px] z-[100]
sm:top-5 md:top-6 md:right-8 md:mt-[64px];
}
.btn-home.is-fixed {
@apply fixed;
}
</style>

View File

@@ -0,0 +1,205 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import type {
DownloadButtonType,
ButtonVariant,
Platform,
} from '#layers/types/components/button'
interface Props {
platform: Platform
type?: DownloadButtonType
variant?: ButtonVariant
backgroundColor?: string
textColor?: string
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
type: 'default',
variant: 'filled',
disabled: false,
})
const runtimeConfig = useRuntimeConfig()
const gameDataStore = useGameDataStore()
const { isProcessing, validateLauncher } = useCheckGameStart()
const { gameData } = storeToRefs(gameDataStore)
const PLATFORM_ICON_MAP: Record<Platform, string> = {
google_play: 'AtomsIconsLogoGoogle',
app_store: 'AtomsIconsLogoApple',
pc: 'AtomsIconsLogoWindow',
stove: 'AtomsIconsLogoStove',
} as const
const DUP_IMAGE_MAP: Record<Platform, string> = {
google_play: '/images/common/btn_logo-google.svg',
app_store: '/images/common/btn_logo-app.svg',
pc: '/images/common/btn_logo-pc.svg',
stove: '/images/common/btn_logo-stove.svg',
} as const
const componentTag = computed(() => {
if (props.type !== 'duplication' && props.platform === 'stove') {
return 'a'
}
return 'button'
})
const isDuplication = computed(() => props.type === 'duplication')
const isSingle = computed(() => props.type === 'single')
const platformIcon = computed(() => PLATFORM_ICON_MAP[props.platform])
const inlineStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {}
if (props.backgroundColor) {
style.backgroundColor = props.backgroundColor
}
if (props.textColor) {
style.color = props.textColor
}
if (props.type === 'duplication') {
style.backgroundImage = `url(${getImageHost(DUP_IMAGE_MAP[props.platform], { imageType: 'common' })})`
}
return style
})
const handleClick = () => {
if (props.platform === 'pc') {
validateLauncher()
return
}
if (props.platform === 'stove' && !isDuplication.value) {
const stoveClientDownloadUrl = runtimeConfig.public.stoveClientDownloadUrl
location.href = stoveClientDownloadUrl
return
}
const url = gameData.value?.market_json[props.platform]?.url
if (url) window.open(url, '_blank')
}
</script>
<template>
<component
:is="componentTag"
v-bind="$attrs"
:class="[
'btn-base',
props.type,
{ 'no-text': isSingle && !$slots.default },
]"
:data-variant="props.variant"
:data-platform="props.platform"
:style="inlineStyle"
:disabled="disabled || isProcessing"
@click="handleClick"
>
<span class="btn-content">
<component
:is="platformIcon"
v-if="!isDuplication"
class="icon-platform"
/>
<span class="text">
<slot />
</span>
<span v-if="type === 'default'" class="icon-download">
<AtomsIconsDownloadLine />
</span>
</span>
</component>
</template>
<style scoped>
.btn-base {
@apply overflow-hidden inline-flex relative font-medium rounded-lg cursor-pointer
before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border before:border-white/10 before:rounded-lg
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:transition-opacity after:duration-300 after:ease-in-out after:opacity-0 after:rounded-lg;
}
.btn-base:hover {
@apply after:opacity-20;
}
.btn-content {
@apply relative flex items-center w-full text-left z-[1];
}
.icon-platform {
@apply w-5 h-5 flex-shrink-0;
}
.btn-base[data-variant='filled'] {
@apply bg-[#383838] text-[#ffffff];
}
.btn-base[data-variant='outlined'] {
@apply bg-white text-[#1F1F1F]
before:border-black/10
after:hidden;
}
.btn-base[data-variant='outlined'][data-platform='app_store'] svg,
.btn-base[data-variant='outlined'][data-platform='pc'] svg,
.btn-base[data-variant='outlined'][data-platform='stove'] svg {
@apply fill-[#1F1F1F];
}
/* default */
.btn-base.default {
@apply w-[296px] py-3.5 px-5
md:w-[356px] md:py-4 md:px-6;
}
.btn-base.default .text {
@apply pl-2 pr-4 line-clamp-2 text-[14px]
md:text-[16px];
}
.btn-base.default .icon-download {
@apply border-l border-white/10 ml-auto pl-4;
}
.btn-base.default[data-platform='stove'] {
@apply bg-[#FC4420];
}
.btn-base.default[data-variant='outlined'] .icon-download {
@apply border-black/10;
}
.btn-base.default[data-variant='outlined'] .icon-download svg {
@apply fill-[#1F1F1F];
}
/* duplication */
.btn-base.duplication {
@apply bg-[16px_50%] bg-[length:auto_28px] bg-no-repeat
pt-[22px] pl-[47px] pr-[22px] pb-[7px] text-[11px]
md:h-[64px] md:pt-[30px] md:pl-[64px] md:pr-[28px] md:pb-[11px] md:text-[12px] md:bg-[20px_50%] md:bg-[length:auto_40px];
}
.btn-base.duplication[data-platform='google_play'] {
@apply min-w-[148px] md:min-w-[194px];
}
.btn-base.duplication[data-platform='app_store'] {
@apply min-w-[132px] md:min-w-[174px];
}
.btn-base.duplication[data-platform='pc'] {
@apply min-w-[113px] md:min-w-[150px];
}
.btn-base.duplication[data-platform='stove'] {
@apply min-w-[113px] md:min-w-[150px];
}
.btn-base.duplication .text {
@apply line-clamp-1;
}
/* single */
.btn-base.single {
@apply justify-center items-center text-[14px]
h-[40px] px-3.5
md:h-[48px];
}
.btn-base.single.no-text {
@apply min-w-[40px] px-0 md:min-w-[48px];
}
.btn-base.single.no-text .icon-platform {
@apply mx-auto;
}
.btn-base.single .icon-platform {
@apply mr-1;
}
</style>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
const { y: windowY } = useWindowScroll({ behavior: 'smooth' })
const showBtn = computed(() => windowY.value > 0)
const handleScrollToTop = () => {
windowY.value = 0
}
</script>
<template>
<Transition name="fade">
<AtomsButtonCircle
v-show="showBtn"
class="btn-top"
sr-only="top"
@click="handleScrollToTop"
/>
</Transition>
</template>
<style scoped>
.btn-top {
@apply bg-[image:var(--button-top)] bg-center bg-cover bg-no-repeat;
}
</style>

View File

@@ -0,0 +1,119 @@
<script setup lang="ts">
const showSnsList = ref(false)
const gameDataStore = useGameDataStore()
const modalStore = useModalStore()
const { gameData } = storeToRefs(gameDataStore)
const { handleOpenToast } = modalStore
const snsBackgroundColor = computed(() => {
const colorData = gameData.value?.comm_sns_bg_color_json?.display
const colorCode = getColorCode({
colorName: colorData?.color_name,
colorCode: colorData?.color_code,
})
return colorCode
})
const snsList = computed(() => {
return gameData.value?.sns_json
})
const handleMouseEnter = () => {
showSnsList.value = true
}
const handleForceClose = () => {
showSnsList.value = false
}
const handleCopy = async () => {
if (!import.meta.client) return
try {
const url = window.location.href
await navigator.clipboard.writeText(url)
handleOpenToast('복사 성공')
} catch (error) {
console.error('복사 실패:', error)
}
}
</script>
<template>
<div v-if="Object.keys(snsList).length > 0" class="sns-container">
<AtomsButtonCircle
class="btn-sns"
sr-only="sns"
:style="{ backgroundColor: snsBackgroundColor }"
@click="handleMouseEnter"
>
<AtomsIconsShareLine class="icon-share" />
</AtomsButtonCircle>
<transition name="fade">
<div
v-if="showSnsList"
class="sns-list"
:style="{ backgroundColor: snsBackgroundColor }"
>
<template v-for="(item, key) in snsList" :key="key">
<a
v-if="item?.url"
:href="item?.url"
target="_blank"
class="sns-item"
:style="{
backgroundImage: `url(${getImageHost(`/images/common/ic-v2-logo-${key}-fill.png`, { imageType: 'common' })})`,
}"
>
<span class="sr-only">{{ key }}</span>
</a>
</template>
<button
type="button"
class="sns-item"
:style="{
backgroundImage: `url(${getImageHost('/images/common/ic-v2-community-link-line.png', { imageType: 'common' })})`,
}"
@click="handleCopy"
>
<span class="sr-only">copy</span>
</button>
<div class="close-container">
<button
type="button"
class="opacity-50 z-[1] hover:opacity-100"
@click="handleForceClose"
>
<span class="sr-only">close</span>
<AtomsIconsCloseLine size="24" />
</button>
</div>
</div>
</transition>
</div>
</template>
<style scoped>
.btn-sns:hover .icon-share {
@apply fill-white;
}
.sns-list {
@apply absolute bottom-0 right-0 flex items-center justify-center gap-4 rounded-full
h-[40px] md:h-[48px] pl-4 pr-3
before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border before:border-[rgba(255,255,255,0.06)] before:rounded-full;
}
.sns-item {
@apply w-[24px] h-[24px] bg-center bg-cover bg-no-repeat opacity-50 z-[1]
hover:opacity-100;
}
.sns-item:hover {
@apply opacity-100;
}
.close-container {
@apply relative flex pl-4
before:content-[''] before:absolute before:top-1/2 before:left-0 before:w-[1px] before:h-[20px] before:bg-[rgba(255,255,255,0.1)] before:translate-y-[-50%];
}
</style>