Init
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">
|
||||
<a :href="link" class="card-link" target="_blank" rel="noopener noreferrer">
|
||||
{{ title }}
|
||||
</a>
|
||||
</h2>
|
||||
<p class="card-description">{{ description }}</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const props = defineProps<{
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
@apply bg-gray-100 shadow-md rounded-lg items-center justify-center border border-gray-300 m-1 w-56;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
@apply p-4;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
@apply text-xl font-semibold mb-2;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
@apply text-gray-700 mb-4;
|
||||
}
|
||||
|
||||
.card-link {
|
||||
@apply text-blue-500 hover:text-blue-700;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<h1 class="title">Generator Labiryntu</h1>
|
||||
|
||||
<div class="input-group">
|
||||
<input v-model="seed" @keyup.enter="generateMaze" type="text" class="input"
|
||||
placeholder="Wprowadź seed (ziarno)">
|
||||
<button @click="generateMaze" class="button" type="button">Generuj</button>
|
||||
</div>
|
||||
|
||||
<div v-if="maze.length" class="maze-container">
|
||||
<div v-for="(row, y) in maze" :key="y" class="maze-row">
|
||||
<div v-for="(cell, x) in row" :key="x" :class="['maze-cell',
|
||||
{
|
||||
'wall': cell === 1,
|
||||
'visited': isVisited(x, y),
|
||||
'start': x === 1 && y === 1,
|
||||
'end': x === maze[0].length - 3 && y === maze.length - 3,
|
||||
'player': x === playerX && y === playerY
|
||||
}]">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mobile-controls" v-if="isMobile">
|
||||
<button @click="moveUp" class="control-button">↑</button>
|
||||
<div class="horizontal-controls">
|
||||
<button @click="moveLeft" class="control-button">←</button>
|
||||
<button @click="moveDown" class="control-button">↓</button>
|
||||
<button @click="moveRight" class="control-button">→</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-counter">
|
||||
Liczba kroków: {{ stepCount }}
|
||||
</div>
|
||||
|
||||
<div v-if="completed">
|
||||
<p class="completion-message mt-2">Gratulacje! Przeszedłeś labirynt.</p>
|
||||
<p class="completion-message">Chcesz spróbować jeszcze raz?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, Ref, onMounted, watch } from 'vue';
|
||||
|
||||
const seed = ref('');
|
||||
const maze = ref<number[][]>([]);
|
||||
|
||||
const playerX = ref(1);
|
||||
const playerY = ref(1);
|
||||
const stepCount = ref(0);
|
||||
|
||||
const completed = ref(false);
|
||||
const visitedCells: Ref<Set<string>> = ref(new Set<string>());
|
||||
|
||||
const isMobile = ref(window.innerWidth < 640);
|
||||
|
||||
const updateIsMobile = () => {
|
||||
isMobile.value = window.innerWidth < 640;
|
||||
};
|
||||
|
||||
window.addEventListener('resize', updateIsMobile);
|
||||
|
||||
const generateMaze = () => {
|
||||
const seedValue = seed.value || Math.random().toString(36).substring(7);
|
||||
const rows = 50;
|
||||
const cols = 50;
|
||||
|
||||
visitedCells.value = new Set();
|
||||
stepCount.value = 0;
|
||||
|
||||
const random = (() => {
|
||||
let seed = hash(seedValue);
|
||||
return () => {
|
||||
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
||||
return seed / 0x7fffffff;
|
||||
};
|
||||
})();
|
||||
|
||||
maze.value = Array(rows).fill(0).map(() => Array(cols).fill(1));
|
||||
|
||||
const stack: [number, number][] = [[1, 1]];
|
||||
maze.value[1][1] = 0;
|
||||
|
||||
while (stack.length > 0) {
|
||||
const [x, y] = stack[stack.length - 1];
|
||||
const directions = [[0, -1], [1, 0], [0, 1], [-1, 0]].sort(() => random() - 0.5);
|
||||
|
||||
let moved = false;
|
||||
for (const [dx, dy] of directions) {
|
||||
const nx = x + dx * 2;
|
||||
const ny = y + dy * 2;
|
||||
if (nx > 0 && nx < cols - 1 && ny > 0 && ny < rows - 1 && maze.value[ny][nx] === 1) {
|
||||
maze.value[ny][nx] = 0;
|
||||
maze.value[y + dy][x + dx] = 0;
|
||||
stack.push([nx, ny]);
|
||||
moved = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!moved)
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
playerX.value = 1;
|
||||
playerY.value = 1;
|
||||
completed.value = false;
|
||||
};
|
||||
|
||||
const movePlayer = (e: KeyboardEvent) => {
|
||||
if (completed.value) return;
|
||||
|
||||
const key = e.key;
|
||||
let newX = playerX.value;
|
||||
let newY = playerY.value;
|
||||
|
||||
if (key === 'ArrowUp') newY--;
|
||||
else if (key === 'ArrowDown') newY++;
|
||||
else if (key === 'ArrowLeft') newX--;
|
||||
else if (key === 'ArrowRight') newX++;
|
||||
|
||||
moveTo(newX, newY);
|
||||
};
|
||||
|
||||
const moveTo = (newX: number, newY: number) => {
|
||||
if (newX >= 0 && newX < maze.value[0].length && newY >= 0 && newY < maze.value.length && maze.value[newY][newX] === 0) {
|
||||
playerX.value = newX;
|
||||
playerY.value = newY;
|
||||
stepCount.value++;
|
||||
visitedCells.value.add(`${newX},${newY}`);
|
||||
|
||||
if (newX === maze.value[0].length - 3 && newY === maze.value.length - 3)
|
||||
completed.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const moveUp = () => moveTo(playerX.value, playerY.value - 1);
|
||||
const moveDown = () => moveTo(playerX.value, playerY.value + 1);
|
||||
const moveLeft = () => moveTo(playerX.value - 1, playerY.value);
|
||||
const moveRight = () => moveTo(playerX.value + 1, playerY.value);
|
||||
|
||||
const hash = (s: string) => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0);
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', movePlayer);
|
||||
generateMaze();
|
||||
});
|
||||
|
||||
watch(completed, (newValue) => {
|
||||
if (newValue)
|
||||
window.removeEventListener('keydown', movePlayer);
|
||||
else
|
||||
window.addEventListener('keydown', movePlayer);
|
||||
});
|
||||
|
||||
const isVisited = (x: number, y: number) => {
|
||||
if (
|
||||
(x == playerX.value && y == playerY.value) ||
|
||||
(x == 1 && y == 1) ||
|
||||
(x == maze.value[0].length - 3 && y == maze.value.length - 3)
|
||||
)
|
||||
return false;
|
||||
return visitedCells.value.has(`${x},${y}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
@apply flex justify-center items-center min-h-screen bg-gray-100 min-w-full p-4;
|
||||
}
|
||||
|
||||
.content {
|
||||
@apply bg-white p-4 sm:p-8 rounded-lg shadow-lg max-w-full;
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply text-2xl sm:text-3xl font-bold text-center mb-4 sm:mb-6;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
@apply flex mb-4;
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply flex-grow px-4 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-blue-500;
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply px-4 py-2 bg-blue-500 text-white rounded-r-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500;
|
||||
}
|
||||
|
||||
.maze-container {
|
||||
@apply inline-block border-2 border-black max-w-full overflow-auto;
|
||||
}
|
||||
|
||||
.maze-row {
|
||||
@apply flex;
|
||||
}
|
||||
|
||||
.maze-cell {
|
||||
@apply w-2 h-2 min-w-0.5 min-h-0.5;
|
||||
}
|
||||
|
||||
.wall {
|
||||
@apply bg-black;
|
||||
}
|
||||
|
||||
.player {
|
||||
@apply bg-red-500;
|
||||
}
|
||||
|
||||
.start {
|
||||
@apply bg-green-500;
|
||||
}
|
||||
|
||||
.end {
|
||||
@apply bg-yellow-500;
|
||||
}
|
||||
|
||||
.visited {
|
||||
@apply bg-blue-200;
|
||||
}
|
||||
|
||||
.step-counter {
|
||||
@apply mt-4 text-center text-base sm:text-lg font-semibold;
|
||||
}
|
||||
|
||||
.completion-message {
|
||||
@apply text-center text-base sm:text-lg font-semibold text-green-600;
|
||||
}
|
||||
|
||||
/* Nowe style dla przycisków na urządzeniach mobilnych */
|
||||
.mobile-controls {
|
||||
@apply mt-4 flex flex-col items-center;
|
||||
}
|
||||
|
||||
.horizontal-controls {
|
||||
@apply flex mt-2 mb-2;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
@apply bg-blue-500 text-white font-bold py-2 px-4 rounded-full m-1;
|
||||
@apply hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<template>
|
||||
<div class="relative">
|
||||
<transition name="slide-fade" @before-enter="beforeEnter" @enter="enter" @leave="leave">
|
||||
<div v-if="isVisible"
|
||||
class="fixed bottom-0 left-0 w-full bg-white shadow-lg rounded-t-lg transition-transform transform"
|
||||
:class="{ 'translate-y-full': !isVisible, 'translate-y-0': isVisible }">
|
||||
<div class="p-4">
|
||||
<p class="mb-3">Moje serwisy.</p>
|
||||
<div class="flex overflow-x-auto">
|
||||
<Card v-for="service in services" :key="service.title" v-bind="service" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<button @click="toggleOffcanvas"
|
||||
class="fixed bottom-4 right-4 w-16 h-16 bg-blue-500 text-white rounded-full flex items-center justify-center shadow-lg hover:bg-blue-600 focus:outline-none">
|
||||
<span class="text-2xl">+</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Card from './Card.vue';
|
||||
|
||||
const isVisible = ref(false);
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: "FileCloud",
|
||||
description: "Moja prywatna chmura plików.",
|
||||
link: "https://filecloud.garandplg.com/ui/core/index.html#/",
|
||||
},
|
||||
{
|
||||
title: "Invidious",
|
||||
description: "Open-source'owy frontend dla YouTube.",
|
||||
link: "https://invidious.garandplg.com",
|
||||
},
|
||||
{
|
||||
title: "Materialious",
|
||||
description: "Polepszony interface dla Invidious.",
|
||||
link: "https://materialious.garandplg.com/?returnYTDislikesInstance=https%3A%2F%2Fryd-proxy.garandplg.com&darkMode=true&themeColor=%23ffff22&autoPlay=true&alwaysLoop=false&proxyVideos=true&listenByDefault=false&savePlaybackPosition=true&dashEnabled=true&theatreModeByDefault=true&autoplayNextByDefault=false&returnYtDislikes=true&searchSuggestions=true&previewVideoOnHover=true&sponsorBlock=true&sponsorBlockUrl=https%3A%2F%2Fsponsor.ajay.app&sponsorBlockCategories=sponsor%2Cselfpromo%2Cinteraction%2Cintro%2Coutro%2Cpreview&deArrowInstance=https%3A%2F%2Fsponsor.ajay.app&deArrowEnabled=true&deArrowThumbnailInstance=https%3A%2F%2Fdearrow-thumb.ajay.app&playerMiniPlayer=true&syncious=true&synciousInstance=https%3A%2F%2Fsyncious.garandplg.com®ion=PL&autoExpandComments=true&autoExpandDesc=false&deArrowTitlesOnly=true&sponsorBlockDisplayToast=true",
|
||||
},
|
||||
{
|
||||
title: "SearXNG",
|
||||
description: "Moja własna meta-wyszukiwarka internetowa.",
|
||||
link: "https://search.garandplg.com/",
|
||||
},
|
||||
{
|
||||
title: "Pterodactyl",
|
||||
description: "Serwis do zarządzania serwerami do gier.",
|
||||
link: "https://pterodactyl-panel.garandplg.com",
|
||||
},
|
||||
{
|
||||
title: "Open WebUI",
|
||||
description: "Własne interface do modeli AI.",
|
||||
link: "https://ai.garandplg.com/",
|
||||
}
|
||||
]
|
||||
|
||||
const toggleOffcanvas = () => isVisible.value = !isVisible.value;
|
||||
|
||||
const beforeEnter = (el: HTMLElement) => el.style.transform = 'translateY(100%)';
|
||||
const enter = (el: HTMLElement) => {
|
||||
el.offsetHeight;
|
||||
el.style.transition = 'transform 0.3s ease-out';
|
||||
el.style.transform = 'translateY(0)';
|
||||
};
|
||||
const leave = (el: HTMLElement) => {
|
||||
el.style.transition = 'transform 0.3s ease-in';
|
||||
el.style.transform = 'translateY(100%)';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.slide-fade-enter-active,
|
||||
.slide-fade-leave-active {
|
||||
@apply transition-transform duration-300 ease-in-out;
|
||||
}
|
||||
|
||||
.slide-fade-enter,
|
||||
.slide-fade-leave-to {
|
||||
@apply translate-y-full;
|
||||
}
|
||||
|
||||
.offcanvas {
|
||||
@apply fixed bottom-0 left-0 w-full bg-white shadow-lg rounded-t-lg;
|
||||
}
|
||||
|
||||
.floating-button {
|
||||
@apply fixed bottom-4 right-4 w-16 h-16 bg-blue-500 text-white rounded-full flex items-center justify-center shadow-lg hover:bg-blue-600 focus:outline-none;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user