feat : dynamic tutorial page
This commit is contained in:
@@ -1,27 +1,19 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Learn from "./components/Leran";
|
import Learn from "./components/Leran";
|
||||||
import ListTutorials from "./components/ListTutorial";
|
import ListTutorials from "./components/ListTutorial";
|
||||||
import { TutorialList } from "../../../types/props";
|
import useAxios from "../../../hooks/useAxios";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
const Tutorials = () => {
|
const Tutorials = () => {
|
||||||
const tutorialList: TutorialList = [
|
const axios = useAxios()
|
||||||
{
|
const {data,error,isLoading,isFetching} = useQuery({
|
||||||
id: 1,
|
queryKey:["categoryLearning"],
|
||||||
name: "آموزش اشتراکی اندروید",
|
queryFn:()=>{
|
||||||
},
|
return axios.get('/home/category/learning')
|
||||||
{
|
}
|
||||||
id: 2,
|
})
|
||||||
name: "آموزش اشتراکی آیفون",
|
console.log(data?.data?.result);
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "آموزش سرویس مولتی",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "آموزش open vpn در اندروید",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const [activeTutorial, setActiveTutorial] = useState<number>(1);
|
const [activeTutorial, setActiveTutorial] = useState<number>(1);
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full flex flex-col px-4 py-4 text-start">
|
<div className="relative w-full h-full flex flex-col px-4 py-4 text-start">
|
||||||
@@ -31,10 +23,13 @@ const Tutorials = () => {
|
|||||||
</p>
|
</p>
|
||||||
<ListTutorials
|
<ListTutorials
|
||||||
activeTutorial={activeTutorial}
|
activeTutorial={activeTutorial}
|
||||||
tutorialList={tutorialList}
|
tutorialList={data?.data?.result}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isFetching={isFetching}
|
||||||
|
error={error}
|
||||||
setActiveTutorial={setActiveTutorial}
|
setActiveTutorial={setActiveTutorial}
|
||||||
/>
|
/>
|
||||||
<Learn activeTutorial={activeTutorial} tutorialList={tutorialList} />
|
<Learn activeTutorial={activeTutorial} tutorialList={data?.data?.result} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,94 +1,83 @@
|
|||||||
import { SyntheticEvent, useEffect, useState } from "react";
|
|
||||||
import { LearnProps } from "../../../../types/props";
|
import { LearnProps } from "../../../../types/props";
|
||||||
import { BiLoaderAlt } from "react-icons/bi";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import useAxios from "../../../../hooks/useAxios";
|
||||||
|
import Alert from "../../../../components/alert/Alert";
|
||||||
|
import { MdErrorOutline } from "react-icons/md";
|
||||||
|
|
||||||
const Learn = ({ activeTutorial, tutorialList }: LearnProps) => {
|
const Learn = ({ activeTutorial }: LearnProps) => {
|
||||||
const tutorialImages = [
|
const axios = useAxios();
|
||||||
{
|
const { data, isLoading, isFetching, error } = useQuery({
|
||||||
id: 1,
|
queryKey: ["learn", activeTutorial],
|
||||||
images: [{ urlimg: "https://persain-game.com/front_end/img/v2ray.png" }],
|
queryFn: () => {
|
||||||
|
return axios.get(`/home/category/learning/${activeTutorial}`);
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
id: 2,
|
|
||||||
images: [
|
|
||||||
{ urlimg: "https://persain-game.com/front_end/img/1-eshterki.jpg" },
|
|
||||||
{ urlimg: "https://persain-game.com/front_end/img/2-eshteraki.jpg" },
|
|
||||||
{ urlimg: "https://persain-game.com/front_end/img/3-eshteraki.jpg" },
|
|
||||||
{ urlimg: "https://persain-game.com/front_end/img/4-eshteraki.jpg" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
images: [
|
|
||||||
{ urlimg: "https://persain-game.com/front_end/img/1-eshterki.jpg" },
|
|
||||||
{ urlimg: "https://persain-game.com/front_end/img/2-eshteraki.jpg" },
|
|
||||||
{ urlimg: "https://persain-game.com/front_end/img/3-eshteraki.jpg" },
|
|
||||||
{ urlimg: "https://persain-game.com/front_end/img/4-eshteraki.jpg" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
images: [
|
|
||||||
{ urlimg: "https://persain-game.com/front_end/img/1-eshterki.jpg" },
|
|
||||||
{ urlimg: "https://persain-game.com/front_end/img/2-eshteraki.jpg" },
|
|
||||||
{ urlimg: "https://persain-game.com/front_end/img/3-eshteraki.jpg" },
|
|
||||||
{ urlimg: "https://persain-game.com/front_end/img/4-eshteraki.jpg" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
|
||||||
const [list, setList] = useState(
|
|
||||||
tutorialList?.filter((id) => id.id === activeTutorial)
|
|
||||||
);
|
|
||||||
const [tutorialImagesState, setTutorialImagesState] = useState(
|
|
||||||
tutorialImages?.filter((id) => id.id === activeTutorial)
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
|
||||||
setList(tutorialList?.filter((id) => id.id === activeTutorial));
|
|
||||||
setTutorialImagesState(
|
|
||||||
tutorialImages?.filter((id) => id.id === activeTutorial)
|
|
||||||
);
|
|
||||||
setLoading(true);
|
|
||||||
}, [activeTutorial]);
|
|
||||||
const handleLoadingImage = (e: SyntheticEvent<HTMLImageElement>) => {
|
|
||||||
if (e.type === "load") setLoading(false);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full border-4 flex flex-col justify-center items-center border-solid mt-4 p-4 rounded-md border-gray-800 border-spacing-1">
|
<div className="w-full h-full border-4 flex flex-col justify-center items-center border-solid mt-4 p-4 rounded-md border-gray-800 border-spacing-1">
|
||||||
{list?.map((i) => {
|
{isLoading || isFetching ? (
|
||||||
return (
|
<div className="w-full h-full p-5 flex justify-center items-center">
|
||||||
<div
|
<svg
|
||||||
key={i.id}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className="relative w-full h-full flex flex-col justify-center items-center"
|
width={100}
|
||||||
|
viewBox="0 0 300 150"
|
||||||
>
|
>
|
||||||
<span className="p-2 bg-gray-800 rounded-md mb-4">{i.name}</span>
|
<path
|
||||||
{loading && (
|
fill="none"
|
||||||
<div className="relative w-full h-60 flex justify-center items-center">
|
className="stroke-gray-600"
|
||||||
<BiLoaderAlt className="w-16 text-white h-16 animate-spin" />
|
strokeWidth="15" // تغییر به camelCase
|
||||||
|
strokeLinecap="round" // تغییر به camelCase
|
||||||
|
strokeDasharray="300 385" // تغییر به camelCase
|
||||||
|
strokeDashoffset="0" // تغییر به camelCase
|
||||||
|
d="M275 75c0 31-27 50-50 50-58 0-92-100-150-100-28 0-50 22-50 50s23 50 50 50c58 0 92-100 150-100 24 0 50 19 50 50Z"
|
||||||
|
>
|
||||||
|
<animate
|
||||||
|
attributeName="stroke-dashoffset"
|
||||||
|
calcMode="spline"
|
||||||
|
dur="2s" // برای مدت زمان، به 's' برای ثانیه نیاز داریم
|
||||||
|
values="685;-685"
|
||||||
|
keySplines="0 0 1 1"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
></animate>
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<Alert type="danger" message="خطایی با این موضوع رخ داد: ">
|
||||||
|
{error.message}
|
||||||
|
</Alert>
|
||||||
|
) : data?.data?.result.length === 0 ? (
|
||||||
|
<div className="w-full h-full flex flex-col gap-3 justify-center items-center">
|
||||||
|
<MdErrorOutline className="w-[5rem] h-[5rem]" />
|
||||||
|
<p className="text-sm md:text-base"> اموزشی وجود ندارد </p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data?.data?.result.map(
|
||||||
|
(i: { id: string; title: string; medias: string[] }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i.id}
|
||||||
|
className="relative w-full h-full flex flex-col justify-center items-center"
|
||||||
|
>
|
||||||
|
<span className="p-2 bg-gray-800 rounded-md mb-4">
|
||||||
|
{i.title}
|
||||||
|
</span>
|
||||||
|
<div className="relative w-full h-full flex flex-col justify-center items-center">
|
||||||
|
{i.medias.map((medias, index) => {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className="w-full md:w-1/2 mb-3 rounded-lg mx-auto h-full"
|
||||||
|
src={medias}
|
||||||
|
alt="images"
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
<div className="relative w-full h-full flex flex-col justify-center items-center">
|
}
|
||||||
{tutorialImagesState?.map((i) => {
|
)
|
||||||
return (
|
)}
|
||||||
<div key={i.id}>
|
|
||||||
{i.images?.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
className="w-full md:w-1/2 mb-3 rounded-lg mx-auto h-full"
|
|
||||||
src={item.urlimg}
|
|
||||||
alt="images"
|
|
||||||
key={index}
|
|
||||||
onLoad={(e) => handleLoadingImage(e)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,47 @@
|
|||||||
|
import { Skeleton } from "@mui/material";
|
||||||
import { ListTutorialsProps } from "../../../../types/props";
|
import { ListTutorialsProps } from "../../../../types/props";
|
||||||
|
import Alert from "../../../../components/alert/Alert";
|
||||||
|
import Button from "../../../../components/button";
|
||||||
|
|
||||||
const ListTutorials = ({
|
const ListTutorials = ({
|
||||||
tutorialList,
|
tutorialList,
|
||||||
activeTutorial,
|
activeTutorial,
|
||||||
|
isLoading,
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
setActiveTutorial,
|
setActiveTutorial,
|
||||||
}: ListTutorialsProps) => {
|
}: ListTutorialsProps) => {
|
||||||
|
if (isLoading || isFetching) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full gap-4 flex flex-wrap justify-center max-sm:grid max-sm:grid-cols-1 mt-4">
|
||||||
|
{Array(3)
|
||||||
|
.fill(null)
|
||||||
|
.map((_, i) => (
|
||||||
|
<div
|
||||||
|
className="flex flex-wrap max-sm:grid max-sm:grid-cols-1 gap-2 justify-center items-center"
|
||||||
|
key={i}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
extra={`!h-auto !text-center !text-sm md:!text-lg !bg-gray-800 !rounded-md !px-2 !py-2 !cursor-pointer`}
|
||||||
|
>
|
||||||
|
<Skeleton animation="pulse" width={150} variant="text" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full mt-4">
|
||||||
|
<Alert type="danger" message="خطایی با این موضوع رخ داد: ">
|
||||||
|
{error.message}
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full mt-4">
|
<div className="w-full h-full mt-4">
|
||||||
<ul className="flex flex-wrap max-sm:grid max-sm:grid-cols-1 gap-2 justify-center items-center">
|
<ul className="flex flex-wrap max-sm:grid max-sm:grid-cols-1 gap-2 justify-center items-center">
|
||||||
|
|||||||
80
src/types/props.d.ts
vendored
80
src/types/props.d.ts
vendored
@@ -5,6 +5,7 @@ export interface AlertProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
type?: "info" | "danger" | "success" | "warning" | "dark";
|
type?: "info" | "danger" | "success" | "warning" | "dark";
|
||||||
message?: string;
|
message?: string;
|
||||||
|
extra?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// begin Button.tsx
|
// begin Button.tsx
|
||||||
@@ -58,12 +59,16 @@ import React, {
|
|||||||
SetStateAction,
|
SetStateAction,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { FormikProps } from "formik";
|
import { FormikProps } from "formik";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
export type TutorialList = { id: number; name: string }[];
|
export type TutorialList = { id: number; name: string }[];
|
||||||
|
|
||||||
export interface ListTutorialsProps {
|
export interface ListTutorialsProps {
|
||||||
tutorialList: TutorialList;
|
tutorialList: TutorialList;
|
||||||
activeTutorial: number;
|
activeTutorial: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
isFetching: boolean;
|
||||||
|
error: Error | AxiosError | null;
|
||||||
setActiveTutorial: SetStateAction;
|
setActiveTutorial: SetStateAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +84,10 @@ export interface TypewriterProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ButtonListProps {
|
export interface ButtonListProps {
|
||||||
ProductsList: { id: number; title: string }[];
|
ProductsList: { id: number; name: string; label?: string }[];
|
||||||
|
isLoading: boolean;
|
||||||
|
isFetching: boolean;
|
||||||
|
error: Error | AxiosError | null;
|
||||||
activeId: number;
|
activeId: number;
|
||||||
setActiveId: SetStateAction;
|
setActiveId: SetStateAction;
|
||||||
}
|
}
|
||||||
@@ -110,6 +118,7 @@ export interface MuiModalProps {
|
|||||||
nameService?: string;
|
nameService?: string;
|
||||||
submitText?: string;
|
submitText?: string;
|
||||||
submitOnClick?: () => void;
|
submitOnClick?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +126,8 @@ export interface ConfigItemProps {
|
|||||||
dateBuy: string;
|
dateBuy: string;
|
||||||
configName: string;
|
configName: string;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
|
isShowTraffic: boolean;
|
||||||
|
textCopy: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// نوع Item برای آیتمهای داخل Dropdown
|
// نوع Item برای آیتمهای داخل Dropdown
|
||||||
@@ -147,3 +158,70 @@ export interface LoginFormValues {
|
|||||||
export interface LoginFromikProps {
|
export interface LoginFromikProps {
|
||||||
formik: FormikProps<LoginFormValues>;
|
formik: FormikProps<LoginFormValues>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BuyResponse {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BuyError {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModalBuyProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
// mutation: UseMutationResult<
|
||||||
|
// AxiosResponse<BuyResponse>,
|
||||||
|
// BuyError,
|
||||||
|
// BuyConfigData,
|
||||||
|
// unknown
|
||||||
|
// >;
|
||||||
|
configId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BuyConfigDataProps {
|
||||||
|
count?: number;
|
||||||
|
discount?: string;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
// تعریف تایپ دادههای واقعی پروفایل کاربر
|
||||||
|
type UserProfile = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
mobile: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// تعریف تایپ پیکربندیها
|
||||||
|
type Config = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
created_at: string | null;
|
||||||
|
show_traffic: boolean;
|
||||||
|
max_traffic: number;
|
||||||
|
traffic: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// تعریف تایپ دادههای برگشتی
|
||||||
|
type ProfileResponseData = {
|
||||||
|
result: {
|
||||||
|
user_profile: UserProfile;
|
||||||
|
enable_config: number;
|
||||||
|
disable_config: number;
|
||||||
|
configs: Config[];
|
||||||
|
};
|
||||||
|
status: boolean;
|
||||||
|
alert: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// تایپ خطا
|
||||||
|
type QueryError = Error | AxiosError | null;
|
||||||
|
|
||||||
|
interface ProfileDetailsProps {
|
||||||
|
data: ProfileResponseData;
|
||||||
|
isLoading: boolean;
|
||||||
|
isFetching: boolean;
|
||||||
|
error: Error | AxiosError | null;
|
||||||
|
userTitle: string;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user