Refactor navbar and remove legacy API/context
Replaces HomeNav with a new SiteNav and associated CSS module, updating navigation structure and user menu. Removes legacy API client, downloader, user model, and UserContext in favor of a new AuthContext stub for future authentication logic. Also cleans up HeroCarousel and minor CSS fixes.
This commit is contained in:
@@ -1,275 +0,0 @@
|
||||
import axios from "axios";
|
||||
|
||||
// --- ENV CONFIG ---
|
||||
const API_BASE_URL =
|
||||
import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
|
||||
|
||||
const LOGIN_PATH = import.meta.env.VITE_LOGIN_PATH || "/login";
|
||||
|
||||
|
||||
// --- ERROR EVENT BUS ---
|
||||
const ERROR_EVENT = "api:error";
|
||||
type ApiErrorDetail = {
|
||||
message: string;
|
||||
status?: number;
|
||||
url?: string;
|
||||
data?: unknown;
|
||||
};
|
||||
|
||||
// Use interface instead of arrow function types for readability
|
||||
interface ApiErrorHandler {
|
||||
(e: CustomEvent<ApiErrorDetail>): void;
|
||||
}
|
||||
|
||||
function notifyError(detail: ApiErrorDetail) {
|
||||
window.dispatchEvent(new CustomEvent<ApiErrorDetail>(ERROR_EVENT, { detail }));
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("[API ERROR]", detail);
|
||||
}
|
||||
function onError(handler: ApiErrorHandler) {
|
||||
const wrapped = handler as EventListener;
|
||||
window.addEventListener(ERROR_EVENT, wrapped as EventListener);
|
||||
|
||||
return () => window.removeEventListener(ERROR_EVENT, wrapped);
|
||||
}
|
||||
|
||||
// --- AXIOS INSTANCES ---
|
||||
// Always send cookies. Django will set auth cookies; browser will include them automatically.
|
||||
function createAxios(baseURL: string): any {
|
||||
const instance = axios.create({
|
||||
baseURL,
|
||||
withCredentials: true, // cookies
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout: 20000,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Use a single behavior for both: cookies are always sent
|
||||
const apiPublic = createAxios(API_BASE_URL);
|
||||
const apiAuth = createAxios(API_BASE_URL);
|
||||
|
||||
|
||||
// --- REQUEST INTERCEPTOR (PUBLIC) ---
|
||||
// Ensure no Authorization header is ever sent by the public client
|
||||
apiPublic.interceptors.request.use(function (config: any) {
|
||||
if (config?.headers && (config.headers as any).Authorization) {
|
||||
delete (config.headers as any).Authorization;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
// --- REQUEST INTERCEPTOR (AUTH) ---
|
||||
// Do not attach Authorization header; rely on cookies set by Django.
|
||||
apiAuth.interceptors.request.use(function (config: any) {
|
||||
(config as any)._retryCount = (config as any)._retryCount || 0;
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
// --- RESPONSE INTERCEPTOR (AUTH) ---
|
||||
// Simplified: on 401, redirect to login. Server manages refresh via cookies.
|
||||
apiAuth.interceptors.response.use(
|
||||
function (response: any) {
|
||||
return response;
|
||||
},
|
||||
async function (error: any) {
|
||||
if (!error.response) {
|
||||
alert("Backend connection is unavailable. Please check your network.");
|
||||
|
||||
notifyError({
|
||||
message: "Network error or backend unavailable",
|
||||
url: error.config?.url,
|
||||
});
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const status = error.response.status;
|
||||
if (status === 401) {
|
||||
ClearTokens();
|
||||
window.location.assign(LOGIN_PATH);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
notifyError({
|
||||
message:
|
||||
(error.response.data as any)?.detail ||
|
||||
(error.response.data as any)?.message ||
|
||||
`Request failed with status ${status}`,
|
||||
status,
|
||||
url: error.config?.url,
|
||||
data: error.response.data,
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// --- PUBLIC CLIENT: still emits errors and alerts on network failure ---
|
||||
apiPublic.interceptors.response.use(
|
||||
function (response: any) {
|
||||
return response;
|
||||
},
|
||||
async function (error: any) {
|
||||
if (!error.response) {
|
||||
alert("Backend connection is unavailable. Please check your network.");
|
||||
notifyError({
|
||||
message: "Network error or backend unavailable",
|
||||
url: error.config?.url,
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
notifyError({
|
||||
message:
|
||||
(error.response.data as any)?.detail ||
|
||||
(error.response.data as any)?.message ||
|
||||
`Request failed with status ${error.response.status}`,
|
||||
status: error.response.status,
|
||||
url: error.config?.url,
|
||||
data: error.response.data,
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
function Logout() {
|
||||
try {
|
||||
const LogOutResponse = apiAuth.post("/api/logout/");
|
||||
|
||||
if (LogOutResponse.body.detail != "Logout successful") {
|
||||
throw new Error("Logout failed");
|
||||
}
|
||||
|
||||
ClearTokens();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error during logout:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function ClearTokens(){
|
||||
document.cookie = "access_token=; Max-Age=0; path=/";
|
||||
document.cookie = "refresh_token=; Max-Age=0; path=/";
|
||||
}
|
||||
|
||||
|
||||
// --- EXPORT DEFAULT API WRAPPER ---
|
||||
const Client = {
|
||||
// Axios instances
|
||||
auth: apiAuth,
|
||||
public: apiPublic,
|
||||
|
||||
Logout,
|
||||
|
||||
// Error subscription
|
||||
onError,
|
||||
};
|
||||
|
||||
export default Client;
|
||||
|
||||
/**
|
||||
USAGE EXAMPLES (TypeScript/React)
|
||||
|
||||
Import the client
|
||||
--------------------------------------------------
|
||||
import Client from "@/api/Client";
|
||||
|
||||
|
||||
Login: obtain tokens and persist to cookies
|
||||
--------------------------------------------------
|
||||
async function login(username: string, password: string) {
|
||||
// SimpleJWT default login endpoint (adjust if your backend differs)
|
||||
// Example backend endpoint: POST /api/token/ -> { access, refresh }
|
||||
const res = await Client.public.post("/api/token/", { username, password });
|
||||
const { access, refresh } = res.data;
|
||||
Client.setTokens(access, refresh);
|
||||
// After this, Client.auth will automatically attach Authorization header
|
||||
// and refresh when receiving a 401 (up to 2 retries).
|
||||
}
|
||||
|
||||
|
||||
Public request (no cookies, no Authorization)
|
||||
--------------------------------------------------
|
||||
// The public client does NOT send cookies or Authorization.
|
||||
async function listPublicItems() {
|
||||
const res = await Client.public.get("/api/public/items/");
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
Authenticated requests (auto Bearer header + refresh on 401)
|
||||
--------------------------------------------------
|
||||
async function fetchProfile() {
|
||||
const res = await Client.auth.get("/api/users/me/");
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function updateProfile(payload: { first_name?: string; last_name?: string }) {
|
||||
const res = await Client.auth.patch("/api/users/me/", payload);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
Global error handling (UI notifications)
|
||||
--------------------------------------------------
|
||||
import { useEffect } from "react";
|
||||
|
||||
function useApiErrors(showToast: (msg: string) => void) {
|
||||
useEffect(function () {
|
||||
const unsubscribe = Client.onError(function (e) {
|
||||
const { message, status } = e.detail;
|
||||
showToast(status ? String(status) + ": " + message : message);
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [showToast]);
|
||||
}
|
||||
|
||||
// Note: Network connectivity issues trigger an alert and also dispatch api:error.
|
||||
// All errors are logged to console for developers.
|
||||
|
||||
|
||||
Logout
|
||||
--------------------------------------------------
|
||||
function logout() {
|
||||
Client.clearTokens();
|
||||
window.location.assign("/login");
|
||||
}
|
||||
|
||||
|
||||
Route protection (PrivateRoute)
|
||||
--------------------------------------------------
|
||||
// If you created src/routes/PrivateRoute.tsx, wrap your protected routes with it.
|
||||
// PrivateRoute checks for "access_token" cookie presence and redirects to /login if missing.
|
||||
|
||||
// Example:
|
||||
// <Routes>
|
||||
// <Route element={<PrivateRoute />} >
|
||||
// <Route element={<MainLayout />}>
|
||||
// <Route path="/" element={<Dashboard />} />
|
||||
// <Route path="/profile" element={<Profile />} />
|
||||
// </Route>
|
||||
// </Route>
|
||||
// <Route path="/login" element={<Login />} />
|
||||
// </Routes>
|
||||
|
||||
|
||||
Refresh and retry flow (what happens on 401)
|
||||
--------------------------------------------------
|
||||
// 1) Client.auth request receives 401 from backend
|
||||
// 2) Client tries to refresh access token using refresh_token cookie
|
||||
// 3) If refresh succeeds, original request is retried (max 2 times)
|
||||
// 4) If still 401 (or no refresh token), tokens are cleared and user is redirected to /login
|
||||
|
||||
|
||||
Environment variables (optional overrides)
|
||||
--------------------------------------------------
|
||||
// VITE_API_BASE_URL default: "http://localhost:8000"
|
||||
// VITE_API_REFRESH_URL default: "/api/token/refresh/"
|
||||
// VITE_LOGIN_PATH default: "/login"
|
||||
*/
|
||||
@@ -1,114 +0,0 @@
|
||||
import Client from "./Client";
|
||||
|
||||
// Available output containers (must match backend)
|
||||
export const FORMAT_EXTS = ["mp4", "mkv", "webm", "flv", "mov", "avi", "ogg"] as const;
|
||||
export type FormatExt = (typeof FORMAT_EXTS)[number];
|
||||
|
||||
export type InfoResponse = {
|
||||
title: string | null;
|
||||
duration: number | null;
|
||||
thumbnail: string | null;
|
||||
video_resolutions: string[]; // e.g. ["2160p", "1440p", "1080p", ...]
|
||||
audio_resolutions: string[]; // e.g. ["320kbps", "160kbps", ...]
|
||||
};
|
||||
|
||||
// GET info for a URL
|
||||
export async function fetchInfo(url: string): Promise<InfoResponse> {
|
||||
const res = await Client.public.get("/api/downloader/download/", {
|
||||
params: { url },
|
||||
});
|
||||
return res.data as InfoResponse;
|
||||
}
|
||||
|
||||
// POST to stream binary immediately; returns { blob, filename }
|
||||
export async function downloadImmediate(args: {
|
||||
url: string;
|
||||
ext: FormatExt;
|
||||
videoResolution?: string | number; // "1080p" or 1080
|
||||
audioResolution?: string | number; // "160kbps" or 160
|
||||
}): Promise<{ blob: Blob; filename: string }> {
|
||||
const video_quality = toHeight(args.videoResolution);
|
||||
const audio_quality = toKbps(args.audioResolution);
|
||||
|
||||
if (video_quality == null || audio_quality == null) {
|
||||
throw new Error("Please select both video and audio quality.");
|
||||
}
|
||||
|
||||
const res = await Client.public.post(
|
||||
"/api/downloader/download/",
|
||||
{
|
||||
url: args.url,
|
||||
ext: args.ext,
|
||||
video_quality,
|
||||
audio_quality,
|
||||
},
|
||||
{ responseType: "blob" }
|
||||
);
|
||||
|
||||
const cd = res.headers?.["content-disposition"] as string | undefined;
|
||||
const xfn = res.headers?.["x-filename"] as string | undefined;
|
||||
const filename =
|
||||
parseContentDispositionFilename(cd) ||
|
||||
(xfn && xfn.trim()) ||
|
||||
inferFilenameFromUrl(args.url, res.headers?.["content-type"] as string | undefined) ||
|
||||
`download.${args.ext}`;
|
||||
|
||||
return { blob: res.data as Blob, filename };
|
||||
}
|
||||
|
||||
// Helpers
|
||||
export function parseContentDispositionFilename(cd?: string): string | null {
|
||||
if (!cd) return null;
|
||||
const utf8Match = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
|
||||
if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
|
||||
const plainMatch = cd.match(/filename\s*=\s*"([^"]+)"/i) || cd.match(/filename\s*=\s*([^;]+)/i);
|
||||
return plainMatch?.[1]?.trim() || null;
|
||||
}
|
||||
|
||||
function inferFilenameFromUrl(url: string, contentType?: string): string {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const last = u.pathname.split("/").filter(Boolean).pop();
|
||||
if (last) return last;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (contentType) {
|
||||
const ext = contentTypeToExt(contentType);
|
||||
return `download${ext ? `.${ext}` : ""}`;
|
||||
}
|
||||
return "download.bin";
|
||||
}
|
||||
|
||||
function contentTypeToExt(ct?: string): string | null {
|
||||
if (!ct) return null;
|
||||
const map: Record<string, string> = {
|
||||
"video/mp4": "mp4",
|
||||
"video/x-matroska": "mkv",
|
||||
"video/webm": "webm",
|
||||
"video/x-flv": "flv",
|
||||
"video/quicktime": "mov",
|
||||
"video/x-msvideo": "avi",
|
||||
"video/ogg": "ogg",
|
||||
"application/octet-stream": "bin",
|
||||
};
|
||||
return map[ct] || null;
|
||||
}
|
||||
|
||||
function toHeight(v?: string | number): number | undefined {
|
||||
if (typeof v === "number") return v || undefined;
|
||||
if (!v) return undefined;
|
||||
const m = /^(\d{2,4})p$/i.exec(v.trim());
|
||||
if (m) return parseInt(m[1], 10);
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? (n as number) : undefined;
|
||||
}
|
||||
|
||||
function toKbps(v?: string | number): number | undefined {
|
||||
if (typeof v === "number") return v || undefined;
|
||||
if (!v) return undefined;
|
||||
const m = /^(\d{2,4})\s*kbps$/i.exec(v.trim());
|
||||
if (m) return parseInt(m[1], 10);
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? (n as number) : undefined;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// frontend/src/api/model/user.js
|
||||
// User API model for searching users by username
|
||||
// Structure matches other model files (see order.js for reference)
|
||||
|
||||
import Client from '../Client';
|
||||
|
||||
const API_BASE_URL = "/account/users";
|
||||
|
||||
const userAPI = {
|
||||
/**
|
||||
* Get current authenticated user
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
async getCurrentUser() {
|
||||
const response = await Client.auth.get(`${API_BASE_URL}/me/`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all users
|
||||
* @returns {Promise<Array<User>>}
|
||||
*/
|
||||
async getUsers(params: Object) {
|
||||
const response = await Client.auth.get(`${API_BASE_URL}/`, { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single user by ID
|
||||
* @param {number|string} id
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
async getUser(id: number) {
|
||||
const response = await Client.auth.get(`${API_BASE_URL}/${id}/`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a user by ID
|
||||
* @param {number|string} id
|
||||
* @param {Object} data
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
async updateUser(id: number, data: Object) {
|
||||
const response = await Client.auth.patch(`${API_BASE_URL}/${id}/`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a user by ID
|
||||
* @param {number|string} id
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async deleteUser(id: number) {
|
||||
const response = await Client.auth.delete(`${API_BASE_URL}/${id}/`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new user
|
||||
* @param {Object} data
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
async createUser(data: Object) {
|
||||
const response = await Client.auth.post(`${API_BASE_URL}/`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Search users by username (partial match)
|
||||
* @param {Object} params - { username: string }
|
||||
* @returns {Promise<Array<User>>}
|
||||
*/
|
||||
async searchUsers(params: { username: string }) {
|
||||
// Adjust the endpoint as needed for your backend
|
||||
const response = await Client.auth.get(`${API_BASE_URL}/`, { params });
|
||||
console.log("User search response:", response.data);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default userAPI;
|
||||
Reference in New Issue
Block a user