swipe game with overlay

This commit is contained in:
TDLaouer 2025-07-13 16:29:02 +02:00
parent f74eb98414
commit 6a818e9da5
10 changed files with 122 additions and 26 deletions

View File

@ -1,10 +1,10 @@
<template> <template>
<motion.img <motion.img
:src="props.card.url" :src="props.card.image"
draggable="false" draggable="false"
class="h-[60cqh] max-h-[800px] w-[400px] origin-bottom rounded-lg object-cover hover:cursor-grab active:cursor-grabbing md:w-[600px]" class="h-[60cqh] max-h-[800px] w-[400px] origin-bottom rounded-lg object-cover hover:cursor-grab active:cursor-grabbing md:w-[600px]"
:style="{ :style="{
zIndex: props.isFront ? 10 : props.z, zIndex: props.isFront ? 500 : props.z,
gridRow: 1, gridRow: 1,
gridColumn: 1, gridColumn: 1,
transition: '0.125s transform', transition: '0.125s transform',
@ -75,11 +75,7 @@ const dragValue = computed(() => (props.isFront ? "x" : false));
const x = useMotionValue(0); const x = useMotionValue(0);
const opacity = useTransform(x, [-150, 0, 150], [0, 1, 0]); const opacity = useTransform(x, [-150, 0, 150], [0, 1, 0]);
const rotateRaw = useTransform(x, [-150, 150], [-18, 18]); const rotate = useTransform(x, [-150, 150], [-18, 18]);
const rotate = useTransform(() => {
const offset = props.isFront ? 0 : props.card.id % 2 ? 4 : -4;
return `${rotateRaw.get() + offset}deg`;
});
const handleDragEnd = () => { const handleDragEnd = () => {
if (Math.abs(x.get()) > 50) { if (Math.abs(x.get()) > 50) {
@ -88,6 +84,7 @@ const handleDragEnd = () => {
}; };
async function swipeRight() { async function swipeRight() {
console.log(props.card.id);
await animate(x, 300, { duration: 0.3 }); await animate(x, 300, { duration: 0.3 });
cardStore.removeCard(props.card.id); cardStore.removeCard(props.card.id);
} }

62
components/UploadView.vue Normal file
View File

@ -0,0 +1,62 @@
<template>
<div class="mt-20 flex flex-col items-center gap-5">
<FloatLabel variant="in">
<InputText id="collection" v-model="collectionName" variant="filled" />
<label for="collection">Collection Name</label>
</FloatLabel>
<FileUpload
name="files[]"
:auto="false"
:multiple="true"
accept="image/*"
custom-upload
@uploader="onUpload"
>
<template #empty>
<p>Drag and drop files here to upload.</p>
</template>
</FileUpload>
</div>
</template>
<script setup lang="ts">
import type { FileUploadUploaderEvent } from "primevue";
const toast = useToast();
const collectionName = ref("");
const onUpload = async (event: FileUploadUploaderEvent) => {
const files = Array.isArray(event.files) ? event.files : [event.files];
if (collectionName.value.trim().length === 0) {
toast.error({
title: "Collection name missing",
message: "Please enter a valid collection name.",
});
return;
}
for (const file of files) {
const formData = new FormData();
formData.append("image", file);
formData.append("name", toTitleCaseWithSpaces(parseImageName(file.name)));
formData.append("collection_name", collectionName.value);
try {
await $fetch("/api/cards/", {
method: "POST",
body: formData,
baseURL: useApiBase(),
});
toast.success({
title: "Success!",
message: "Files uploaded successfully",
});
} catch (e) {
console.error(e);
toast.error({
title: "Upload failed!",
message: "Something went wrong.",
});
}
}
};
</script>

View File

@ -0,0 +1 @@
export const useApiBase = () => useRuntimeConfig().public.apiBase;

View File

@ -15,8 +15,10 @@ export default defineNuxtConfig({
], ],
css: ["~/assets/css/main.css"], css: ["~/assets/css/main.css"],
vite: { plugins: [tailwindcss()] }, vite: { plugins: [tailwindcss()] },
app: { runtimeConfig: {
baseURL: process.env.BASE_URL || "/", public: {
apiBase: process.env.API_BASE_URL || "http://localhost:8000",
},
}, },
primevue: { primevue: {
components: { components: {
@ -35,4 +37,4 @@ export default defineNuxtConfig({
}, },
}, },
}, },
}); });

View File

@ -4,7 +4,7 @@
v-for="(card, index) in cardStore.cards" v-for="(card, index) in cardStore.cards"
:key="card.id" :key="card.id"
:card="card" :card="card"
:is-front="index === cardStore.cards.length - 1" :is-front="index === 0"
:z="index" :z="index"
/> />
</div> </div>
@ -15,6 +15,8 @@ import CardView from "~/components/CardView.vue";
import { useCardStore } from "~/store/Card"; import { useCardStore } from "~/store/Card";
const route = useRoute(); const route = useRoute();
const collection = route.query.collection; const collection: string = route.query.collection as string;
const cardStore = useCardStore(); const cardStore = useCardStore();
cardStore.fetchCardsByCollection(collection);
</script> </script>

View File

@ -1,13 +1,13 @@
<template> <template>
<div class="flex min-h-screen flex-col items-center justify-center"> <div class="flex min-h-screen flex-col items-center justify-center gap-5">
<h1 class="mb-8 text-3xl font-bold">Select a collection to begin</h1> <h1 class="mb-8 text-3xl font-bold">Select a collection to begin</h1>
<ul class="w-full max-w-md space-y-4"> <ul class="w-full max-w-md space-y-4">
<li <li
v-for="collection in cardStore.collections" v-for="collection in cardStore.collections"
:key="collection" :key="collection.name"
class="flex items-center justify-between rounded p-4 shadow" class="flex items-center justify-between rounded p-4 shadow"
> >
<span class="text-lg font-medium">{{ collection }}</span> <span class="text-lg font-bold">{{ collection.name }}</span>
<!-- Large Play Button --> <!-- Large Play Button -->
<Button <Button
icon="pi pi-play" icon="pi pi-play"
@ -22,10 +22,11 @@
width 0.3s, width 0.3s,
height 0.3s; height 0.3s;
" "
@click="play(collection)" @click="play(collection.name)"
/> />
</li> </li>
</ul> </ul>
<UploadView />
</div> </div>
</template> </template>
@ -34,8 +35,8 @@ import { useCardStore } from "~/store/Card";
const cardStore = useCardStore(); const cardStore = useCardStore();
cardStore.collections = ["Test", "Test1"]; cardStore.fetchCollections();
/*
cardStore.cards = [ cardStore.cards = [
{ {
id: 1, id: 1,
@ -84,7 +85,7 @@ cardStore.cards = [
name: "Pipi", name: "Pipi",
url: "https://images.unsplash.com/photo-1505784045224-1247b2b29cf3?q=80&w=2340&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", url: "https://images.unsplash.com/photo-1505784045224-1247b2b29cf3?q=80&w=2340&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
}, },
]; ]; */
const activeItem = ref(); const activeItem = ref();

View File

@ -1,20 +1,35 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import type { Card } from "~/types/Card"; import type { Card } from "~/types/Card";
import type { Collection } from "~/types/Collection";
export const useCardStore = defineStore("cardStore", { export const useCardStore = defineStore("cardStore", {
state: () => ({ state: () => ({
collections: [] as string[], collections: [] as Collection[],
cards: [] as Card[], cards: [] as Card[],
smashList: [] as Card[],
passList: [] as Card[],
}), }),
actions: { actions: {
async fetchCollections() { async fetchCollections() {
const { data: collections } = const data = await $fetch<Collection[]>("/api/collections/", {
await useFetch<string[]>("/api/collections"); baseURL: useApiBase(),
this.collections = collections.value ?? []; });
this.collections = data ?? [];
}, },
async fetchCardsByCollection(collectionName: string) { async fetchCardsByCollection(collectionName: string) {
const response = useFetch<Card[]>(`/api/cards/${collectionName}`); const data = await $fetch<Card[]>(
this.cards = response.data.value ?? []; `/api/cards/?collection__name=${collectionName}`,
{
baseURL: useApiBase(),
},
);
this.cards =
data.map((card, index) => ({
...card,
id: index,
})) ?? [];
console.log(this.cards);
}, },
removeCard(id: number) { removeCard(id: number) {
const index = this.cards.findIndex((card) => card.id === id); const index = this.cards.findIndex((card) => card.id === id);

View File

@ -1,5 +1,5 @@
export type Card = { export type Card = {
id: number; id: number;
url: string; image: string;
name: string; name: string;
}; };

3
types/Collection.ts Normal file
View File

@ -0,0 +1,3 @@
export type Collection = {
name: string;
};

13
utils/fileNameParser.ts Normal file
View File

@ -0,0 +1,13 @@
export const toTitleCaseWithSpaces = (input: string): string => {
return input
.replace(/[-_]/g, " ")
.split(" ")
.filter(Boolean)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
};
export const parseImageName = (fileName: string): string => {
// Extract the name without the extension
return fileName.split(".").slice(0, -1).join(".");
};