feat: duration time based expiry date for links
All checks were successful
Update changelog / changelog (push) Successful in 27s

This commit is contained in:
Pc
2026-01-19 17:22:31 +01:00
parent efc2861ab4
commit 0afbdaf7f8

View File

@@ -2,7 +2,8 @@
import { import {
PawPrint, Heart, Sparkles, Cat, Hash, PawPrint, Heart, Sparkles, Cat, Hash,
Globe, BookOpen, Shield, Clock, Globe, BookOpen, Shield, Clock,
Settings2, AlertCircle, X, Save, RefreshCw, Copy, Check, ExternalLink, User as UserIcon Settings2, AlertCircle, X, Save, RefreshCw, Copy, Check, ExternalLink,
User as UserIcon, Calendar, Hourglass
} from 'lucide-react'; } from 'lucide-react';
const API_BASE = import.meta.env.VITE_API_TARGET; const API_BASE = import.meta.env.VITE_API_TARGET;
@@ -10,6 +11,15 @@ const TOKEN_KEY = 'jwt_token';
type CaseType = 'upper' | 'lower' | 'mixed'; type CaseType = 'upper' | 'lower' | 'mixed';
// Mnożniki czasu w milisekundach
const TIME_MULTIPLIERS: Record<string, number> = {
minutes: 60 * 1000,
hours: 60 * 60 * 1000,
days: 24 * 60 * 60 * 1000,
weeks: 7 * 24 * 60 * 60 * 1000,
months: 30 * 24 * 60 * 60 * 1000, // Przybliżenie
};
interface GeneratorProps { interface GeneratorProps {
url: string; url: string;
setUrl: (url: string) => void; setUrl: (url: string) => void;
@@ -62,13 +72,17 @@ export const Generator: React.FC<GeneratorProps> = ({ url, setUrl, onGenerate })
withSubdomain: false withSubdomain: false
}); });
// --- NOWE STANY DLA CZASU TRWANIA ---
const [expiryMode, setExpiryMode] = useState<'date' | 'duration'>('date');
const [durationValue, setDurationValue] = useState<string>(''); // Pusty string na start
const [durationUnit, setDurationUnit] = useState<string>('minutes');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [generatingUri, setGeneratingUri] = useState(false); const [generatingUri, setGeneratingUri] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null); const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [result, setResult] = useState<string | null>(null); const [result, setResult] = useState<string | null>(null);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
// Obliczanie domeny bazowej i prefiksu do wyświetlenia w polu URI
const baseDomain = API_BASE.replace('api.', '').replace(/^https?:\/\//, '').split('/')[0]; const baseDomain = API_BASE.replace('api.', '').replace(/^https?:\/\//, '').split('/')[0];
const displayPrefix = genSettings.withSubdomain && formData.subdomain const displayPrefix = genSettings.withSubdomain && formData.subdomain
? `${formData.subdomain}.${baseDomain}` ? `${formData.subdomain}.${baseDomain}`
@@ -105,6 +119,30 @@ export const Generator: React.FC<GeneratorProps> = ({ url, setUrl, onGenerate })
checkUser(); checkUser();
}, []); }, []);
// --- NOWY EFEKT: Przeliczanie czasu trwania na datę ---
// Uruchamia się automatycznie, gdy zmienisz liczbę, jednostkę lub tryb
useEffect(() => {
if (expiryMode === 'duration') {
const val = parseInt(durationValue);
if (!durationValue || isNaN(val) || val <= 0) {
// Jeśli pole jest puste lub 0, czyścimy datę wygaśnięcia
setFormData(prev => ({ ...prev, expiryDate: '' }));
return;
}
const multiplier = TIME_MULTIPLIERS[durationUnit] || TIME_MULTIPLIERS.minutes;
const msToAdd = val * multiplier;
const futureDate = new Date(Date.now() + msToAdd);
// Formatowanie do datetime-local z uwzględnieniem strefy czasowej
const offset = futureDate.getTimezoneOffset() * 60000;
const localISOTime = new Date(futureDate.getTime() - offset).toISOString().slice(0, 16);
setFormData(prev => ({ ...prev, expiryDate: localISOTime }));
}
}, [durationValue, durationUnit, expiryMode]);
const handleGenerateUri = async (type: 'random' | 'wordlist') => { const handleGenerateUri = async (type: 'random' | 'wordlist') => {
setGeneratingUri(true); setGeneratingUri(true);
setErrorMsg(null); setErrorMsg(null);
@@ -154,12 +192,10 @@ export const Generator: React.FC<GeneratorProps> = ({ url, setUrl, onGenerate })
privacy: formData.privacy, privacy: formData.privacy,
}; };
// Dodajemy subdomenę tylko jeśli jest włączona
if (genSettings.withSubdomain && formData.subdomain) { if (genSettings.withSubdomain && formData.subdomain) {
payload.subdomain = formData.subdomain; payload.subdomain = formData.subdomain;
} }
// WYSYŁANIE DATY TYLKO JEŚLI JEST PODANA
if (formData.expiryDate) { if (formData.expiryDate) {
payload.expiryDate = new Date(formData.expiryDate).getTime(); payload.expiryDate = new Date(formData.expiryDate).getTime();
} }
@@ -175,7 +211,6 @@ export const Generator: React.FC<GeneratorProps> = ({ url, setUrl, onGenerate })
const data = await response.json(); const data = await response.json();
if (!response.ok) throw new Error(data.error || `Error ${response.status}`); if (!response.ok) throw new Error(data.error || `Error ${response.status}`);
// Budowanie końcowego linku do wyświetlenia (z subdomeną)
let finalLink = data.url; let finalLink = data.url;
if (!finalLink) { if (!finalLink) {
const protocol = API_BASE.startsWith('https') ? 'https://' : 'http://'; const protocol = API_BASE.startsWith('https') ? 'https://' : 'http://';
@@ -294,22 +329,18 @@ export const Generator: React.FC<GeneratorProps> = ({ url, setUrl, onGenerate })
</div> </div>
<div className="relative flex items-center"> <div className="relative flex items-center">
{/* Dynamiczna domena przed / */}
<div className="absolute left-4 flex items-center pointer-events-none z-10"> <div className="absolute left-4 flex items-center pointer-events-none z-10">
<span className="text-pink-300 font-bold text-sm sm:text-lg">{displayPrefix}</span> <span className="text-pink-300 font-bold text-sm sm:text-lg">{displayPrefix}</span>
<span className="text-pink-400 font-black text-lg mx-1">/</span> <span className="text-pink-400 font-black text-lg mx-1">/</span>
</div> </div>
<input <input
type="text" type="text"
placeholder="my-custom-uri" placeholder="my-custom-uri"
// Padding-left obliczany na podstawie długości domeny
style={{ paddingLeft: `${displayPrefix.length * 9.5 + 40}px` }} style={{ paddingLeft: `${displayPrefix.length * 9.5 + 40}px` }}
className="w-full p-4 bg-white border-2 border-pink-200 rounded-2xl outline-none focus:border-pink-500 transition-all text-pink-600 font-black text-lg shadow-inner" className="w-full p-4 bg-white border-2 border-pink-200 rounded-2xl outline-none focus:border-pink-500 transition-all text-pink-600 font-black text-lg shadow-inner"
value={formData.uri} value={formData.uri}
onChange={(e) => setFormData({ ...formData, uri: e.target.value })} onChange={(e) => setFormData({ ...formData, uri: e.target.value })}
/> />
{formData.uri && ( {formData.uri && (
<div className="absolute right-4 top-1/2 -translate-y-1/2"> <div className="absolute right-4 top-1/2 -translate-y-1/2">
<Check size={18} className="text-green-500" /> <Check size={18} className="text-green-500" />
@@ -321,18 +352,73 @@ export const Generator: React.FC<GeneratorProps> = ({ url, setUrl, onGenerate })
{/* 3. Database Settings */} {/* 3. Database Settings */}
<div className="mb-8 flex flex-col gap-4"> <div className="mb-8 flex flex-col gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Datetime input (Data + Godzina) */}
<div className="bg-gray-50 p-4 rounded-2xl border border-gray-100"> {/* ZMODYFIKOWANA SEKCJA: Expiry Date / Custom Duration */}
<label className="flex items-center gap-2 text-[10px] font-black uppercase text-gray-400 mb-2"> <div className="bg-gray-50 p-4 rounded-2xl border border-gray-100 flex flex-col">
<Clock size={14} /> Expiry Date & Time <div className="flex items-center justify-between mb-2">
<label className="flex items-center gap-2 text-[10px] font-black uppercase text-gray-400">
<Clock size={14} /> Expiry Time
</label> </label>
{/* Przełącznik trybu */}
<div className="flex bg-gray-200 rounded-lg p-0.5">
<button
onClick={() => setExpiryMode('date')}
className={`p-1.5 rounded-md transition-all ${expiryMode === 'date' ? 'bg-white text-pink-500 shadow-sm' : 'text-gray-400 hover:text-gray-600'}`}
>
<Calendar size={14} />
</button>
<button
onClick={() => setExpiryMode('duration')}
className={`p-1.5 rounded-md transition-all ${expiryMode === 'duration' ? 'bg-white text-pink-500 shadow-sm' : 'text-gray-400 hover:text-gray-600'}`}
>
<Hourglass size={14} />
</button>
</div>
</div>
{expiryMode === 'date' ? (
<>
<input <input
type="datetime-local" type="datetime-local"
className="w-full bg-white border border-gray-200 rounded-lg px-3 py-2 text-xs font-bold text-gray-600 outline-none focus:border-pink-400" className="w-full bg-white border border-gray-200 rounded-lg px-3 py-3 text-xs font-bold text-gray-600 outline-none focus:border-pink-400 mb-1"
value={formData.expiryDate} value={formData.expiryDate}
onChange={e => setFormData({ ...formData, expiryDate: e.target.value })} onChange={e => setFormData({ ...formData, expiryDate: e.target.value })}
/> />
<p className="text-[8px] text-gray-400 mt-1 uppercase">Optional: Leave empty for no expiry</p> <p className="text-[8px] text-gray-400 uppercase pl-1">Pick specific date</p>
</>
) : (
<>
{/* NOWE: Custom Duration Inputs dla Mobile */}
<div className="flex gap-2 mb-1">
<input
type="number"
inputMode="numeric"
pattern="[0-9]*"
placeholder="45"
min="1"
className="w-1/2 bg-white border border-gray-200 rounded-lg px-3 py-3 text-sm font-bold text-pink-500 outline-none focus:border-pink-400 placeholder:text-gray-300 text-center"
value={durationValue}
onChange={(e) => setDurationValue(e.target.value)}
/>
<select
className="w-1/2 bg-white border border-gray-200 rounded-lg px-2 py-3 text-xs font-bold text-gray-600 outline-none focus:border-pink-400"
value={durationUnit}
onChange={(e) => setDurationUnit(e.target.value)}
>
<option value="minutes">Minutes</option>
<option value="hours">Hours</option>
<option value="days">Days</option>
<option value="weeks">Weeks</option>
<option value="months">Months</option>
</select>
</div>
<p className="text-[8px] text-gray-400 uppercase truncate pl-1">
{formData.expiryDate
? `Expires: ${new Date(formData.expiryDate).toLocaleString()}`
: 'Enter duration (e.g. 45 min)'}
</p>
</>
)}
</div> </div>
<label className="flex items-center justify-between bg-gray-50 p-4 rounded-2xl border border-gray-100 cursor-pointer hover:bg-pink-50 transition-colors"> <label className="flex items-center justify-between bg-gray-50 p-4 rounded-2xl border border-gray-100 cursor-pointer hover:bg-pink-50 transition-colors">