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 {
|
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>
|
<label className="flex items-center gap-2 text-[10px] font-black uppercase text-gray-400">
|
||||||
<input
|
<Clock size={14} /> Expiry Time
|
||||||
type="datetime-local"
|
</label>
|
||||||
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"
|
{/* Przełącznik trybu */}
|
||||||
value={formData.expiryDate}
|
<div className="flex bg-gray-200 rounded-lg p-0.5">
|
||||||
onChange={e => setFormData({ ...formData, expiryDate: e.target.value })}
|
<button
|
||||||
/>
|
onClick={() => setExpiryMode('date')}
|
||||||
<p className="text-[8px] text-gray-400 mt-1 uppercase">Optional: Leave empty for no expiry</p>
|
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-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 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">
|
||||||
|
|||||||
Reference in New Issue
Block a user