feat: add users question

This commit is contained in:
2026-08-20 18:48:22 +03:30
parent aba556618b
commit 96c74601c8
6 changed files with 365 additions and 11 deletions
+27 -2
View File
@@ -1,7 +1,11 @@
// Thin fetch wrapper for the meditation API. The app is a static SPA, so every
// call runs in the browser and carries the bearer token from localStorage.
import { MEDITATION_BASE_URL, TOKEN_STORAGE_KEY } from "./config";
import {
APPRO_TOKEN_STORAGE_KEY,
MEDITATION_BASE_URL,
TOKEN_STORAGE_KEY,
} from "./config";
export class ApiError extends Error {
status: number;
@@ -29,6 +33,21 @@ export function clearToken() {
window.localStorage.removeItem(TOKEN_STORAGE_KEY);
}
export function getApproToken(): string | null {
if (typeof window === "undefined") return null;
return window.localStorage.getItem(APPRO_TOKEN_STORAGE_KEY);
}
export function setApproToken(token: string) {
if (typeof window === "undefined") return;
window.localStorage.setItem(APPRO_TOKEN_STORAGE_KEY, token);
}
export function clearApproToken() {
if (typeof window === "undefined") return;
window.localStorage.removeItem(APPRO_TOKEN_STORAGE_KEY);
}
type Query = Record<string, string | number | boolean | undefined | null>;
interface RequestOptions {
@@ -40,6 +59,8 @@ interface RequestOptions {
baseUrl?: string;
// Skip attaching the bearer token (used by the login calls).
auth?: boolean;
// Extra headers to merge into the request.
headers?: Record<string, string>;
}
function buildUrl(path: string, query?: Query, baseUrl = MEDITATION_BASE_URL) {
@@ -60,7 +81,7 @@ export async function apiFetch<T = unknown>(
path: string,
options: RequestOptions = {},
): Promise<T> {
const { method = "GET", body, query, baseUrl, auth = true } = options;
const { method = "GET", body, query, baseUrl, auth = true, headers: extraHeaders } = options;
const headers: Record<string, string> = { Accept: "application/json" };
@@ -69,6 +90,10 @@ export async function apiFetch<T = unknown>(
if (token) headers.Authorization = `Bearer ${token}`;
}
if (extraHeaders) {
Object.assign(headers, extraHeaders);
}
let payload: BodyInit | undefined;
if (body instanceof FormData) {
payload = body; // browser sets multipart Content-Type + boundary
+6 -8
View File
@@ -13,7 +13,6 @@ import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { APPRO_BASE_URL, MEDITATION_BASE_URL, PACKAGE_NAME } from "./config";
@@ -21,8 +20,10 @@ import {
ApiError,
apiFetch,
clearToken,
clearApproToken,
getToken,
setToken,
setApproToken,
toFormData,
} from "./api";
@@ -73,16 +74,12 @@ async function meditationLogin(approoToken: string): Promise<string> {
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [token, setTokenState] = useState<string | null>(null);
const [ready, setReady] = useState(false);
useEffect(() => {
setTokenState(getToken());
setReady(true);
}, []);
const [token, setTokenState] = useState<string | null>(() => getToken());
const [ready] = useState(true);
const login = useCallback(async (identifier: string, password: string) => {
const approoToken = await approAgencyLogin(identifier, password);
setApproToken(approoToken);
const medToken = await meditationLogin(approoToken);
setToken(medToken);
setTokenState(medToken);
@@ -90,6 +87,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const logout = useCallback(() => {
clearToken();
clearApproToken();
setTokenState(null);
}, []);
+2 -1
View File
@@ -19,5 +19,6 @@ export const ASSET_BASE_URL =
process.env.NEXT_PUBLIC_ASSET_BASE_URL ??
MEDITATION_BASE_URL.replace(/\/api\/?$/, "");
// localStorage key for the meditation bearer token.
// localStorage keys for the meditation bearer token and approagency token.
export const TOKEN_STORAGE_KEY = "meditation_admin_token";
export const APPRO_TOKEN_STORAGE_KEY = "approagency_admin_token";
+6
View File
@@ -14,6 +14,7 @@ import {
TagIcon,
TimerIcon,
TrophyIcon,
UserIcon,
WorryIcon,
} from "@/components/icons";
@@ -113,6 +114,11 @@ export const NAV: NavSection[] = [
icon: SurveyIcon,
items: [{ href: "/dashboard/surveys", label: "پرسش‌های نظرسنجی" }],
},
{
label: "کاربران",
icon: UserIcon,
items: [{ href: "/dashboard/users", label: "کاربران نظرسنجی" }],
},
{
label: "گفتگو با مشاور",
icon: ChatIcon,