Skip to content

Commit

Permalink
feat: simple shop page (#304)
Browse files Browse the repository at this point in the history
  • Loading branch information
hmbanan666 authored Dec 18, 2024
1 parent 345c217 commit eab1087
Show file tree
Hide file tree
Showing 21 changed files with 219 additions and 73 deletions.
1 change: 1 addition & 0 deletions apps/telegram-game/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ declare global {
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useBackButton: typeof import('./src/composables/useBackButton')['useBackButton']
const useCharacters: typeof import('./src/composables/useCharacters')['useCharacters']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
Expand Down
4 changes: 4 additions & 0 deletions apps/telegram-game/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ActiveCard: typeof import('./src/components/ActiveCard.vue')['default']
Button: typeof import('./src/components/Button.vue')['default']
CharacterActivationBlock: typeof import('./src/components/CharacterActivationBlock.vue')['default']
ComingSoon: typeof import('./src/components/ComingSoon.vue')['default']
Game: typeof import('./src/components/Game.vue')['default']
GameNavigator: typeof import('./src/components/GameNavigator.vue')['default']
Expand All @@ -15,5 +18,6 @@ declare module 'vue' {
PageContainer: typeof import('./src/components/PageContainer.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SectionHeader: typeof import('./src/components/SectionHeader.vue')['default']
}
}
Binary file added apps/telegram-game/public/coin-small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/telegram-game/public/coin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/telegram-game/public/coupon-small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions apps/telegram-game/src/components/ActiveCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template>
<div class="relative h-full tg-section-bg aspect-square p-4 rounded-2xl cursor-pointer active:scale-90 duration-200" @click="handleClick()">
<slot />
</div>
</template>

<script setup lang="ts">
import { hapticFeedback } from '@telegram-apps/sdk-vue'
function handleClick() {
if (hapticFeedback.impactOccurred.isAvailable()) {
hapticFeedback.impactOccurred('light')
}
}
</script>
15 changes: 15 additions & 0 deletions apps/telegram-game/src/components/Button.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template>
<button class="p-3 tg-button w-full rounded-2xl font-medium cursor-pointer active:scale-95 duration-200" @click="handleClick()">
<slot />
</button>
</template>

<script setup lang="ts">
import { hapticFeedback } from '@telegram-apps/sdk-vue'
function handleClick() {
if (hapticFeedback.impactOccurred.isAvailable()) {
hapticFeedback.impactOccurred('light')
}
}
</script>
28 changes: 28 additions & 0 deletions apps/telegram-game/src/components/CharacterActivationBlock.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<div v-if="isActive" class="px-8 tg-hint text-center font-medium leading-tight">
Это твой активный персонаж
</div>
<Button v-else class="mt-3" @click="activateCharacter()">
Активировать
</Button>
</template>

<script setup lang="ts">
import { useFetch } from '@vueuse/core'
const { characterId } = defineProps<{
characterId: string
}>()
const { characters, refreshCharacters } = useCharacters()
const { profile, refreshProfile } = useTelegramProfile()
const character = computed(() => characters.value?.find(({ id }) => id === characterId))
const isActive = computed(() => profile.value.profile?.activeEditionId === character.value?.editions?.find(({ profileId }) => profileId === profile.value?.profile.id)?.id)
async function activateCharacter() {
await useFetch(`https://chatgame.space/api/telegram/profile/${profile.value.id}/character/${characterId}/activate`).get().json<{ ok: boolean }>()
await refreshProfile()
await refreshCharacters()
}
</script>
4 changes: 2 additions & 2 deletions apps/telegram-game/src/components/Game.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
<div class="max-w-[28rem] mx-auto px-4">
<GameNavigator :player-x="game.player?.x" :wagon-x="game.wagon?.x" />

<div v-if="profile?.energy >= 0" class="w-fit px-5 py-1 flex flex-row items-center gap-2 bg-orange-100/80 text-amber-600 rounded-full">
<div v-if="profile?.energy >= 0" class="w-fit h-10 px-5 py-0 flex flex-row items-center gap-2 bg-orange-100/80 text-amber-600 rounded-full">
<img src="/energy.png" alt="avatar" class="w-auto h-8">
<p class="text-2xl font-semibold leading-none tracking-tight">
<p class="text-xl font-semibold leading-none tracking-tight">
{{ profile?.energy }}
</p>
</div>
Expand Down
4 changes: 2 additions & 2 deletions apps/telegram-game/src/components/GameNavigator.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<template>
<div class="relative mb-2 h-8 bg-red-950/25 text-white rounded-full">
<div class="relative mb-2 h-10 bg-red-950/15 text-white rounded-full">
<div v-if="wagonOnNavigator > 1 && wagonOnNavigator < 99" class="absolute transform -translate-x-1/2" :style="{ left: `${wagonOnNavigator}%` }">
<div class="-mt-1 p-1 bg-purple-300 rounded-lg">
<img src="/wheel-1.png" alt="" class="w-8 h-8">
</div>
</div>

<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<div class="mt-6 mx-auto w-2 h-10 bg-red-900 rounded-lg" />
<div class="mt-6 mx-auto w-2 h-12 bg-red-900 rounded-lg" />
<div class="min-w-12 h-6 flex flex-col justify-center items-center gap-0 text-red-950">
<p class="text-sm font-medium">
{{ Math.floor(playerX ? playerX / 50 : 0) }} м
Expand Down
26 changes: 8 additions & 18 deletions apps/telegram-game/src/components/Modal.vue
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
<template>
<div
class="z-40 left-0 right-0 top-0 bottom-0 tg-secondary-bg opacity-0 transition-all duration-600"
class="z-40 left-0 right-0 top-0 bottom-0 tg-secondary-bg opacity-0 transition-all duration-500"
:class="{ 'fixed! block! opacity-70': isOpened, 'opacity-0!': isClosing }"
/>
<div
class="z-40 flex flex-col justify-end fixed left-0 right-0 bottom-0 mx-auto w-full max-w-lg max-h-[100dvh] overflow-y-auto p-4 m-0 pb-20 shadow-none translate-y-full transition-transform duration-500"
:class="{ 'top-0! translate-y-0!': isOpened, 'translate-y-full!': isClosing }"
>
<div ref="target" class="mb-10 p-4 md:p-6 lg:p-8 tg-section-bg tg-text rounded-2xl shadow-lg max-h-[70dvh] overflow-y-auto">
<div class="mb-4 flex flex-row justify-between items-center">
<h3 class="text-xl md:text-2xl font-medium">
{{ title }}
</h3>
</div>
<div ref="target" class="relative mb-10 p-4 md:p-6 lg:p-8 space-y-3 tg-section-bg tg-text rounded-2xl shadow-lg max-h-[70dvh] overflow-y-auto !overflow-visible">
<h3 class="text-xl md:text-2xl font-medium leading-tight">
{{ title }}
</h3>

<slot />

<button class="mt-4 p-3 tg-button w-full rounded-2xl font-medium" @click="onClose()">
<Button @click="onClose()">
Закрыть
</button>
</Button>
</div>
</div>
</template>

<script setup lang="ts">
import { hapticFeedback } from '@telegram-apps/sdk-vue'
import { onClickOutside, usePointerSwipe } from '@vueuse/core'
const { isOpened } = defineProps<{
Expand All @@ -46,17 +43,10 @@ watch(isSwiping, () => {
})
function onClose() {
handleFeedback()
isClosing.value = true
setTimeout(() => {
emit('close')
isClosing.value = false
}, 600)
}
function handleFeedback() {
if (hapticFeedback.impactOccurred.isAvailable()) {
hapticFeedback.impactOccurred('light')
}
}, 500)
}
</script>
9 changes: 9 additions & 0 deletions apps/telegram-game/src/components/SectionHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<h2 class="mb-2 tg-section-header-text text-2xl">
{{ text }}
</h2>
</template>

<script setup lang="ts">
defineProps<{ text: string }>()
</script>
8 changes: 8 additions & 0 deletions apps/telegram-game/src/composables/useCharacters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { CharacterWithEditions } from '@chat-game/types'
import { useFetch } from '@vueuse/core'

const { data, execute: refreshCharacters } = useFetch('https://chatgame.space/api/character').get().json<CharacterWithEditions[]>()

export function useCharacters() {
return { characters: data, refreshCharacters }
}
21 changes: 12 additions & 9 deletions apps/telegram-game/src/composables/useTelegramProfile.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { initData } from '@telegram-apps/sdk-vue'
import { useFetch } from '@vueuse/core'

export function useTelegramProfile() {
const user = initData.user()

const { data } = useFetch(`https://chatgame.space/api/telegram/${user?.id}?username=${user?.username}`, {
async onFetchError(ctx) {
return ctx
},
}).get().json()
const { data, execute: refreshProfile } = useFetch(`https://chatgame.space/api/telegram/`, {
async beforeFetch() {
const user = initData.user()
if (user) {
return {
url: `https://chatgame.space/api/telegram/${user.id}?username=${user.username}`,
}
}
},
}).get().json()

return { profile: data }
export function useTelegramProfile() {
return { profile: data, refreshProfile }
}
16 changes: 7 additions & 9 deletions apps/telegram-game/src/views/InventoryView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,14 @@
</div>

<div class="grid grid-cols-3 gap-2">
<div v-for="item in items" :key="item.id" class="h-full tg-section-bg aspect-square p-4 rounded-2xl cursor-pointer" @click="isOpened = true">
<div class="relative">
<img :src="item.img" alt="" class="w-full h-auto">
<div class="absolute -bottom-2 -right-2">
<p class="mx-auto w-fit px-3 py-1 tg-secondary-bg rounded-full text-xl leading-none">
{{ item.amount }}
</p>
</div>
<ActiveCard v-for="item in items" :key="item.id" @click="isOpened = true">
<img :src="item.img" alt="" class="w-full h-auto">
<div class="absolute bottom-0 right-0">
<p class="mx-auto w-fit px-3 py-2 tg-secondary-bg rounded-tl-2xl rounded-br-2xl text-xl leading-none">
{{ item.amount }}
</p>
</div>
</div>
</ActiveCard>
</div>
</PageContainer>

Expand Down
5 changes: 2 additions & 3 deletions apps/telegram-game/src/views/QuestView.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
<template>
<PageContainer>
<h2 class="mb-2 tg-section-header-text text-2xl">
Активные комнаты
</h2>
<SectionHeader text="Активные комнаты" />

<div class="flex flex-col gap-2">
<div v-for="room in rooms" :key="room.id" class="tg-section-bg mb-4 px-3 py-3 flex flex-col gap-2 items-center rounded-2xl">
Expand All @@ -24,6 +22,7 @@
</template>

<script setup lang="ts">
import SectionHeader from '@/components/SectionHeader.vue'
import { hapticFeedback } from '@telegram-apps/sdk-vue'
import { gameClient, roomConnected } from '../utils/gameClient'
Expand Down
66 changes: 64 additions & 2 deletions apps/telegram-game/src/views/ShopView.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,71 @@
<template>
<PageContainer>
<ComingSoon />
<div class="tg-section-bg mb-4 px-3 py-3 flex flex-row gap-2 items-center rounded-2xl">
<img src="/coin.png" alt="" class="w-14 h-14">
<div>
<div class="text-2xl font-medium">
{{ profile?.profile?.coins }}
</div>
<div class="tg-hint text-sm">
Монеты
</div>
</div>
</div>

<SectionHeader text="Коллекция персонажей 2024" />

<div class="grid grid-cols-2 gap-2">
<ActiveCard v-for="char in characters" :key="char?.id" @click="() => { isCharacterOpened = true; selectedCharacterId = char?.id }">
<div v-if="!char?.editions?.find(({ profileId }) => profileId === profile?.profile.id)" class="z-10 absolute top-0 left-0 right-0 bottom-0 tg-secondary-bg opacity-40" />

<div v-if="profile?.profile?.activeEditionId === char?.editions?.find(({ profileId }) => profileId === profile?.profile.id)?.id" class="tg-accent-text text-base font-medium leading-tight">
Активный
</div>
<p class="font-medium text-lg">
{{ char?.nickname }}
</p>
<div v-if="char?.price && !char?.editions?.find(({ profileId }) => profileId === profile?.profile.id)" class="flex flex-row gap-1 items-center">
<img src="/coin-small.png" alt="" class="w-5 h-5 grayscale-100">
<p>{{ char?.price }}</p>
</div>

<img :src="`/units/${char?.codename}/128.png`" alt="" class="absolute bottom-0 right-0 w-32 h-auto" :class="{ 'grayscale-100 opacity-70': !char?.editions?.find(({ profileId }) => profileId === profile?.profile.id) }">
</ActiveCard>
</div>
</PageContainer>

<Modal :title="`&laquo;${selectedCharacter?.nickname}&raquo; ${selectedCharacter?.name}`" :is-opened="isCharacterOpened" @close="isCharacterOpened = false">
<img :src="`/units/${selectedCharacter?.codename}/idle.gif`" alt="" class="absolute -top-24 left-0 w-28 h-28">

<p class="text-sm">
{{ selectedCharacter?.description }}
</p>

<div v-if="selectedCharacter?.editions?.find(({ profileId }) => profileId === profile?.profile.id)">
<CharacterActivationBlock :character-id="selectedCharacterId" />
</div>
<div v-else>
<Button v-if="selectedCharacter?.price" class="mt-3 flex flex-row gap-2 items-center justify-center" @click="() => {}">
<p>Разблокировать за</p>
<div class="flex flex-row gap-2 items-center text-xl">
<p>{{ selectedCharacter?.price }}</p>
<img src="/coin-small.png" alt="" class="w-8 h-8">
</div>
</Button>
<div v-else class="px-8 tg-hint text-center font-medium leading-tight">
Персонажа нельзя разблокировать за Монеты
</div>
</div>
</Modal>
</template>

<script setup lang="ts">
import ComingSoon from '@/components/ComingSoon.vue'
import CharacterActivationBlock from '@/components/CharacterActivationBlock.vue'
const { profile } = useTelegramProfile()
const { characters } = useCharacters()
const isCharacterOpened = ref(false)
const selectedCharacterId = ref()
const selectedCharacter = computed(() => characters.value?.find(({ id }) => id === selectedCharacterId.value))
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ export default defineEventHandler(async (event) => {
const profile = await prisma.telegramProfile.findFirst({
where: { telegramId },
include: {
profile: true,
profile: {
include: {
characterEditions: true,
},
},
},
})
if (!profile) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export default defineEventHandler(async (event) => {
const telegramId = getRouterParam(event, 'telegramId')
const characterId = getRouterParam(event, 'characterId')

const telegramProfile = await prisma.telegramProfile.findFirst({
where: { id: telegramId },
include: {
profile: {
include: {
characterEditions: true,
},
},
},
})
if (!telegramProfile || !telegramProfile?.profile) {
throw createError({
status: 404,
})
}

const edition = telegramProfile.profile.characterEditions.find((e) => e.characterId === characterId)
if (!edition) {
throw createError({
status: 400,
message: 'You do not have this character',
})
}

await prisma.profile.update({
where: { id: telegramProfile.profile.id },
data: {
activeEditionId: edition.id,
},
})

return { ok: true }
})
Loading

0 comments on commit eab1087

Please sign in to comment.