mirror of
https://github.com/mmahdium/TorrentMax.git
synced 2026-08-14 17:22:49 +03:30
Finished API integration
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
totalTrackers: number;
|
||||
magnetUrl: string;
|
||||
}>();
|
||||
|
||||
const copied = ref(false);
|
||||
|
||||
function copyMagnet() {
|
||||
navigator.clipboard.writeText(decodeURIComponent(props.magnetUrl));
|
||||
copied.value = true;
|
||||
setTimeout(() => (copied.value = false), 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-2xl mx-auto ">
|
||||
<div class="rounded-2xl border bg-base-100 shadow-lg p-6 space-y-4">
|
||||
<!-- Title and trackers -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold wrap-break-word flex-1">
|
||||
{{ name }}
|
||||
</h2>
|
||||
<span class="badge badge-outline whitespace-nowrap ml-3">
|
||||
{{ totalTrackers }} trackers
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Magnet link with copy -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Maxed Out Magnet Link</span>
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="url"
|
||||
:value="decodeURIComponent(props.magnetUrl)"
|
||||
readonly
|
||||
class="input input-bordered w-full font-medium text-xs"
|
||||
/>
|
||||
<button
|
||||
v-auto-animate
|
||||
class="btn btn-square btn-primary text-accent"
|
||||
@click="copyMagnet"
|
||||
:title="copied ? 'Copied!' : 'Copy magnet link'"
|
||||
>
|
||||
<!-- Default copy icon -->
|
||||
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M15.666 3.888A2.25 2.25 0 0 0 13.5
|
||||
2.25h-3c-1.03 0-1.9.693-2.166
|
||||
1.638m7.332 0c.055.194.084.4.084.612v0a.75.75
|
||||
0 0 1-.75.75H9a.75.75 0 0
|
||||
1-.75-.75v0c0-.212.03-.418.084-.612m7.332
|
||||
0c.646.049 1.288.11 1.927.184 1.1.128
|
||||
1.907 1.077 1.907 2.185V19.5a2.25
|
||||
2.25 0 0 1-2.25 2.25H6.75A2.25
|
||||
2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057
|
||||
1.907-2.185a48.208 48.208 0 0 1
|
||||
1.927-.184" />
|
||||
</svg>
|
||||
|
||||
<!-- Success icon -->
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M11.35 3.836c-.065.21-.1.433-.1.664
|
||||
0 .414.336.75.75.75h4.5a.75.75
|
||||
0 0 0 .75-.75 2.25 2.25 0 0
|
||||
0-.1-.664m-5.8 0A2.251 2.251
|
||||
0 0 1 13.5 2.25H15c1.012 0
|
||||
1.867.668 2.15 1.586m-5.8
|
||||
0c-.376.023-.75.05-1.124.08C9.095
|
||||
4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05
|
||||
1.124.08 1.131.094 1.976 1.057
|
||||
1.976 2.192V16.5A2.25 2.25
|
||||
0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621
|
||||
0-1.125.504-1.125 1.125v11.25c0
|
||||
.621.504 1.125 1.125 1.125h9.75c.621
|
||||
0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621
|
||||
0 1.125.504 1.125 1.125v9.375m-8.25-3
|
||||
1.5 1.5 3-3.75" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info alert -->
|
||||
<div class="alert alert-soft mt-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21
|
||||
12a9 9 0 11-18 0 9 9 0 0118
|
||||
0z"/>
|
||||
</svg>
|
||||
<span>
|
||||
This magnet link has been enriched with additional trackers for better availability.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const emit = defineEmits(["submit"]);
|
||||
|
||||
const magnetUrl = ref("");
|
||||
const error = ref("");
|
||||
|
||||
function isMagnetLink(url: string): boolean {
|
||||
const magnetRegex =
|
||||
/^magnet:\?xt=urn:btih:(?:[A-F0-9]{40}|[A-Z2-7]{32})(?:&[a-z0-9]+=[^&]*)*$/i;
|
||||
return magnetRegex.test(url.trim());
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!isMagnetLink(magnetUrl.value)) {
|
||||
error.value = "Invalid magnet URL";
|
||||
return;
|
||||
}
|
||||
error.value = "";
|
||||
emit("submit", magnetUrl.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="flex items-center w-full rounded-2xl shadow-lg px-4 py-2 transition-all duration-500 focus-within:ring-2"
|
||||
:class="
|
||||
error
|
||||
? 'border-2 border-red-500 bg-base-200'
|
||||
: 'border border-primary bg-base-100 focus-within:ring-primary'
|
||||
"
|
||||
>
|
||||
<!-- Left icon -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-5 mr-3 transition-colors duration-500"
|
||||
:class="error ? 'text-red-500' : 'text-gray-400'"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- Input -->
|
||||
<input
|
||||
v-model="magnetUrl"
|
||||
type="text"
|
||||
placeholder="Enter magnet URL"
|
||||
class="flex-1 bg-transparent outline-none text-base"
|
||||
/>
|
||||
|
||||
<!-- Right circular button -->
|
||||
<button
|
||||
class="ml-3 w-10 h-10 flex items-center justify-center rounded-full bg-primary text-accent hover:scale-110 transition-transform duration-300 shadow-md"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Error text -->
|
||||
<p v-motion-fade v-if="error" class="mt-2 text-sm text-red-500">
|
||||
{{ error }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
+84
-181
@@ -1,201 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useMotion } from '@vueuse/motion'
|
||||
import MagnetCard from "~/components/MagnetCard.vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
const magnetInput = ref('')
|
||||
const isLoading = ref(false)
|
||||
|
||||
// Light bar particles
|
||||
interface Particle {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
opacity: number;
|
||||
speed: number;
|
||||
size: number;
|
||||
symbol: string;
|
||||
interface MaxedMagnetResponse {
|
||||
totalTrackers: number;
|
||||
name?: string;
|
||||
id: string;
|
||||
maxedMagnet: string;
|
||||
}
|
||||
|
||||
const particles = ref<Particle[]>([])
|
||||
const maxedMagnet = ref<MaxedMagnetResponse | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
if (process.client) {
|
||||
initializeParticles()
|
||||
}
|
||||
})
|
||||
async function getMaxedMagnetUrl(magnet: string) {
|
||||
error.value = null; // Clear any previous error
|
||||
loading.value = true; // Set loading to true
|
||||
|
||||
const symbols = ['✦', '✧', '★', '●', '◈', '◇', '◈']
|
||||
try {
|
||||
const response = await fetch("/api/magnet", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ magnet }),
|
||||
});
|
||||
|
||||
const initializeParticles = () => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
const particleCount = 20
|
||||
particles.value = []
|
||||
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
particles.value.push({
|
||||
id: i,
|
||||
x: Math.random() * window.innerWidth,
|
||||
y: Math.random() * 60, // Start from top area where light bar is
|
||||
opacity: Math.random() * 0.4 + 0.2,
|
||||
speed: Math.random() * 0.8 + 0.2,
|
||||
size: Math.random() * 8 + 4,
|
||||
symbol: symbols[Math.floor(Math.random() * symbols.length)]
|
||||
})
|
||||
}
|
||||
|
||||
// Start animation
|
||||
animateParticles()
|
||||
}
|
||||
|
||||
let animationFrameId: number
|
||||
|
||||
const animateParticles = () => {
|
||||
particles.value.forEach(particle => {
|
||||
// Move particles down
|
||||
particle.y += particle.speed
|
||||
|
||||
// Reset particles that go off screen
|
||||
if (particle.y > window.innerHeight) {
|
||||
particle.y = Math.random() * 20
|
||||
particle.x = Math.random() * window.innerWidth
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
error.value =
|
||||
errorData.message || `HTTP error! status: ${response.status}`;
|
||||
console.error("API error:", error.value);
|
||||
return;
|
||||
}
|
||||
})
|
||||
|
||||
// Continue animation
|
||||
animationFrameId = requestAnimationFrame(animateParticles)
|
||||
}
|
||||
|
||||
const addTrackers = async () => {
|
||||
if (!magnetInput.value.trim()) return
|
||||
|
||||
isLoading.value = true
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
isLoading.value = false
|
||||
|
||||
// Here you would implement the actual tracker addition logic
|
||||
console.log('Adding trackers to:', magnetInput.value)
|
||||
}
|
||||
|
||||
// Clean up animation on component unmount
|
||||
onUnmounted(() => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
maxedMagnet.value = await response.json();
|
||||
} catch (err) {
|
||||
error.value =
|
||||
err instanceof Error ? err.message : "An unknown error occurred";
|
||||
console.error("Network error:", err);
|
||||
} finally {
|
||||
loading.value = false; // Reset loading state
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-black text-white overflow-hidden relative">
|
||||
<!-- Single expanding radial gradient for seamless light -->
|
||||
<div
|
||||
v-motion
|
||||
:initial="{ width: '0px', height: '0px', top: '-20%', left: '50%', transform: 'translateX(-50%)' }"
|
||||
:visibleOnce="{
|
||||
width: '200vw',
|
||||
height: '200vh',
|
||||
transition: { duration: 3000, ease: 'cubic-bezier(0.3, 0.7, 0.4, 1.0)' }
|
||||
}"
|
||||
class="absolute z-0 opacity-40"
|
||||
:style="{
|
||||
background: 'radial-gradient(circle at 50% 20%, rgba(59, 130, 246, 0.5), rgba(139, 92, 246, 0.25) 20%, transparent 50%)',
|
||||
'background-size': '100% 100%',
|
||||
'background-repeat': 'no-repeat'
|
||||
}"
|
||||
></div>
|
||||
<!-- Additional bright center glow -->
|
||||
<div
|
||||
v-motion
|
||||
:initial="{ opacity: 0 }"
|
||||
:visibleOnce="{ opacity: 0.5, transition: { duration: 500, delay: 300 } }"
|
||||
class="absolute top-[-10%] left-1/2 transform -translate-x-1/2 w-80 h-80 rounded-full bg-blue-400 blur-3xl z-0"
|
||||
></div>
|
||||
|
||||
<!-- Falling particles -->
|
||||
<div class="absolute inset-0 z-0">
|
||||
<div
|
||||
v-for="particle in particles"
|
||||
:key="particle.id"
|
||||
class="absolute text-blue-300/60 text-lg font-bold select-none pointer-events-none"
|
||||
:style="{
|
||||
left: particle.x + 'px',
|
||||
top: particle.y + 'px',
|
||||
opacity: particle.opacity,
|
||||
fontSize: particle.size + 'px',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}"
|
||||
>
|
||||
{{ particle.symbol }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col min-h-screen bg-base-300">
|
||||
<div class="grow flex items-start justify-center pt-40 px-4">
|
||||
<div class="w-full max-w-2xl space-y-6">
|
||||
<div class="hero">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-5xl font-bold drop-shadow-xl">TorrentMax</h1>
|
||||
<p class="py-6">
|
||||
Paste a magnet link below and TorrentMax will enrich it with
|
||||
additional trackers
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SearchBar @submit="getMaxedMagnetUrl" />
|
||||
|
||||
<!-- Content -->
|
||||
<div class="relative z-10 min-h-screen flex flex-col items-center justify-center p-6">
|
||||
<!-- Title with subtle fade in -->
|
||||
<div
|
||||
v-motion
|
||||
:initial="{ opacity: 0 }"
|
||||
:visibleOnce="{ opacity: 1, transition: { duration: 1000 } }"
|
||||
class="text-center mb-10"
|
||||
>
|
||||
<h1 class="text-4xl md:text-6xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-500 mb-4 tracking-tight">
|
||||
TorrentMax
|
||||
</h1>
|
||||
<p class="text-gray-400 max-w-md mx-auto">
|
||||
Maximize your torrents with premium tracker integration
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Input and Button -->
|
||||
<div
|
||||
v-motion
|
||||
:initial="{ opacity: 0 }"
|
||||
:visibleOnce="{ opacity: 1, transition: { duration: 1000, delay: 300 } }"
|
||||
class="w-full max-w-2xl flex flex-col sm:flex-row gap-4"
|
||||
>
|
||||
<textarea
|
||||
v-model="magnetInput"
|
||||
placeholder="Enter magnet link or .torrent file content..."
|
||||
class="flex-grow px-6 py-4 rounded-xl bg-gray-800/50 backdrop-blur-sm border border-gray-700 focus:border-blue-400 focus:ring-2 focus:ring-blue-500/30 text-white placeholder-gray-500 min-h-[60px] max-h-[120px] resize-none shadow-inner focus:shadow-blue-500/10 focus:shadow-lg transition-all duration-300"
|
||||
rows="1"
|
||||
></textarea>
|
||||
<button
|
||||
@click="addTrackers"
|
||||
:disabled="isLoading"
|
||||
class="px-8 py-4 bg-gradient-to-r from-blue-600 to-purple-600 rounded-xl font-medium text-white shadow-lg shadow-blue-500/20 hover:shadow-blue-500/40 hover:from-blue-500 hover:to-purple-600 transition-all duration-500 disabled:opacity-50 flex items-center justify-center whitespace-nowrap"
|
||||
<!-- Loading indicator -->
|
||||
<div
|
||||
v-motion-fade
|
||||
v-if="loading"
|
||||
class="flex justify-center items-center p-8"
|
||||
>
|
||||
<span v-if="!isLoading" class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||||
</svg>
|
||||
Max It Out
|
||||
</span>
|
||||
<span v-else class="flex items-center">
|
||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Processing...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="loading loading-infinity loading-lg"></span>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 text-center text-sm text-gray-600">
|
||||
<p>Your torrents will be enhanced with premium trackers for maximum performance</p>
|
||||
<!-- Error message display -->
|
||||
<div v-if="error && !loading" class="max-w-2xl mx-auto p-4">
|
||||
<div class="alert alert-error">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MagnetCard
|
||||
v-motion-fade
|
||||
v-if="maxedMagnet && !loading"
|
||||
:name="maxedMagnet.name || 'Unknown Torrent'"
|
||||
:total-trackers="maxedMagnet.totalTrackers"
|
||||
:magnet-url="maxedMagnet.maxedMagnet"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Simple fade in animation */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 1s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
+3
-1
@@ -1,2 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
@plugin "daisyui" {
|
||||
themes: lemonade --default, abyss --prefersdark;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user