feat: duration time based expiry date for links
All checks were successful
Update changelog / changelog (push) Successful in 27s
All checks were successful
Update changelog / changelog (push) Successful in 27s
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
import {
|
||||
PawPrint, Heart, Sparkles, Cat, Hash,
|
||||
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';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_TARGET;
|
||||
@@ -10,6 +11,15 @@ const TOKEN_KEY = 'jwt_token';
|
||||
|
||||
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 {
|
||||
url: string;
|
||||
setUrl: (url: string) => void;
|
||||
@@ -62,13 +72,17 @@ export const Generator: React.FC<GeneratorProps> = ({ url, setUrl, onGenerate })
|
||||
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 [generatingUri, setGeneratingUri] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
const [result, setResult] = useState<string | null>(null);
|
||||
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 displayPrefix = genSettings.withSubdomain && formData.subdomain
|
||||
? `${formData.subdomain}.${baseDomain}`
|
||||
@@ -105,6 +119,30 @@ export const Generator: React.FC<GeneratorProps> = ({ url, setUrl, onGenerate })
|
||||
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') => {
|
||||
setGeneratingUri(true);
|
||||
setErrorMsg(null);
|
||||
@@ -154,12 +192,10 @@ export const Generator: React.FC<GeneratorProps> = ({ url, setUrl, onGenerate })
|
||||
privacy: formData.privacy,
|
||||
};
|
||||
|
||||
// Dodajemy subdomenę tylko jeśli jest włączona
|
||||
if (genSettings.withSubdomain && formData.subdomain) {
|
||||
payload.subdomain = formData.subdomain;
|
||||
}
|
||||
|
||||
// WYSYŁANIE DATY TYLKO JEŚLI JEST PODANA
|
||||
if (formData.expiryDate) {
|
||||
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();
|
||||
if (!response.ok) throw new Error(data.error || `Error ${response.status}`);
|
||||
|
||||
// Budowanie końcowego linku do wyświetlenia (z subdomeną)
|
||||
let finalLink = data.url;
|
||||
if (!finalLink) {
|
||||
const protocol = API_BASE.startsWith('https') ? 'https://' : 'http://';
|
||||
@@ -294,22 +329,18 @@ export const Generator: React.FC<GeneratorProps> = ({ url, setUrl, onGenerate })
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center">
|
||||
{/* Dynamiczna domena przed / */}
|
||||
<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-400 font-black text-lg mx-1">/</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="my-custom-uri"
|
||||
// Padding-left obliczany na podstawie długości domeny
|
||||
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"
|
||||
value={formData.uri}
|
||||
onChange={(e) => setFormData({ ...formData, uri: e.target.value })}
|
||||
/>
|
||||
|
||||
{formData.uri && (
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2">
|
||||
<Check size={18} className="text-green-500" />
|
||||
@@ -321,18 +352,73 @@ export const Generator: React.FC<GeneratorProps> = ({ url, setUrl, onGenerate })
|
||||
{/* 3. Database Settings */}
|
||||
<div className="mb-8 flex flex-col 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">
|
||||
<label className="flex items-center gap-2 text-[10px] font-black uppercase text-gray-400 mb-2">
|
||||
<Clock size={14} /> Expiry Date & Time
|
||||
|
||||
{/* ZMODYFIKOWANA SEKCJA: Expiry Date / Custom Duration */}
|
||||
<div className="bg-gray-50 p-4 rounded-2xl border border-gray-100 flex flex-col">
|
||||
<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>
|
||||
{/* 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
|
||||
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}
|
||||
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>
|
||||
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user