This commit is contained in:
GarandPLG
2024-08-28 15:35:35 +02:00
parent 7f91ea313f
commit 96650aa674
23 changed files with 4930 additions and 0 deletions
+47
View File
@@ -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>
+250
View File
@@ -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>
+95
View File
@@ -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&region=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>