AW-119-personal-video

show generated personal video on /v1/trial-payment page
This commit is contained in:
gofnnp 2024-06-24 17:06:52 +04:00
parent 93896b9b19
commit 1430b67f31
22 changed files with 375 additions and 17 deletions

View File

@ -5,4 +5,5 @@ AURA_SITE_HOST=https://aura.wit.life
AURA_PREFIX=api/v1
AURA_OPEN_AI_HOST=https://api.openai.com
AURA_OPEN_AI_PREFIX=v1
AURA_YANDEX_COUNTER_NUMBER=95799066
AURA_YANDEX_COUNTER_NUMBER=95799066
AURA_PERSONAL_VIDEO_TIME_LIMIT=180

View File

@ -5,4 +5,5 @@ AURA_SITE_HOST=https://aura.wit.life
AURA_PREFIX=api/v1
AURA_OPEN_AI_HOST=https://api.openai.com
AURA_OPEN_AI_PREFIX=v1
AURA_YANDEX_COUNTER_NUMBER=95799066
AURA_YANDEX_COUNTER_NUMBER=95799066
AURA_PERSONAL_VIDEO_TIME_LIMIT=120

71
package-lock.json generated
View File

@ -25,6 +25,7 @@
"react-dom": "^18.2.0",
"react-ga4": "^2.1.0",
"react-i18next": "^12.3.1",
"react-player": "^2.16.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.11.2",
"react-slick": "^0.30.2",
@ -1678,6 +1679,14 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -2685,6 +2694,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/load-script": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
"integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA=="
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -2736,6 +2750,11 @@
"resolved": "https://registry.npmjs.org/md5-es/-/md5-es-1.8.2.tgz",
"integrity": "sha512-LKq5jmKMhJYhsBFUh2w+J3C4bMiC5uQie/UYJ429UATmMnFr6iANO2uQq5HXAZSIupGp0WO2mH3sNfxR4XO40Q=="
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -3069,6 +3088,11 @@
"react": "^18.2.0"
}
},
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
},
"node_modules/react-ga4": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-2.1.0.tgz",
@ -3100,6 +3124,21 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
"node_modules/react-player": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.0.tgz",
"integrity": "sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==",
"dependencies": {
"deepmerge": "^4.0.0",
"load-script": "^1.0.0",
"memoize-one": "^5.1.1",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.0.1"
},
"peerDependencies": {
"react": ">=16.6.0"
}
},
"node_modules/react-property": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz",
@ -4766,6 +4805,11 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
},
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -5508,6 +5552,11 @@
"type-check": "~0.4.0"
}
},
"load-script": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
"integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA=="
},
"locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -5550,6 +5599,11 @@
"resolved": "https://registry.npmjs.org/md5-es/-/md5-es-1.8.2.tgz",
"integrity": "sha512-LKq5jmKMhJYhsBFUh2w+J3C4bMiC5uQie/UYJ429UATmMnFr6iANO2uQq5HXAZSIupGp0WO2mH3sNfxR4XO40Q=="
},
"memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -5774,6 +5828,11 @@
"scheduler": "^0.23.0"
}
},
"react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
},
"react-ga4": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-2.1.0.tgz",
@ -5793,6 +5852,18 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
"react-player": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.0.tgz",
"integrity": "sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==",
"requires": {
"deepmerge": "^4.0.0",
"load-script": "^1.0.0",
"memoize-one": "^5.1.1",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.0.1"
}
},
"react-property": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz",

View File

@ -31,6 +31,7 @@
"react-dom": "^18.2.0",
"react-ga4": "^2.1.0",
"react-i18next": "^12.3.1",
"react-player": "^2.16.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.11.2",
"react-slick": "^0.30.2",

View File

@ -29,6 +29,7 @@ import {
Palmistry,
Paywall,
Payment,
UserVideos,
} from './resources'
const api = {
@ -80,6 +81,8 @@ const api = {
getPaywallByPlacementKey: createMethod<Paywall.PayloadGet, Paywall.ResponseGet>(Paywall.createRequestGet),
// Payment
makePayment: createMethod<Payment.PayloadPost, Payment.ResponsePost>(Payment.createRequestPost),
// User videos
getUserVideos: createMethod<UserVideos.PayloadGet, UserVideos.ResponseGet>(UserVideos.createRequest),
}
export type ApiContextValue = typeof api

View File

@ -178,6 +178,8 @@ export interface ICreateAuthorizePayload {
export interface ICreateAuthorizeResponse {
token: string;
userId?: string;
generatingVideo?: boolean;
videoId?: string;
}
export const createAuthorizeRequest = (data: ICreateAuthorizePayload): Request => {

View File

@ -0,0 +1,23 @@
import routes from "@/routes";
import { getAuthHeaders } from "../utils";
interface Payload {
token: string;
}
export type PayloadGet = Payload;
export interface IUserVideo {
id: string,
videoUrl: string,
createdAt: string
}
type ResponseGetSuccess = IUserVideo[];
export type ResponseGet = ResponseGetSuccess;
export const createRequest = ({ token }: PayloadGet): Request => {
const url = new URL(routes.server.getUserVideos());
return new Request(url, { method: "GET", headers: getAuthHeaders(token) });
};

View File

@ -27,3 +27,4 @@ export * as Products from "./Products";
export * as Palmistry from "./Palmistry";
export * as Paywall from "./Paywall";
export * as Payment from "./Payment";
export * as UserVideos from "./UserVideos";

View File

@ -7,6 +7,9 @@ import { onboardingTitles } from "../../data/onboarding";
import ProgressBarLine from "@/components/ui/ProgressBarLine";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall";
import { usePersonalVideo } from "@/hooks/personalVideo/usePersonalVideo";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
function OnboardingPage() {
const navigate = useNavigate();
@ -17,21 +20,29 @@ function OnboardingPage() {
const [progress, setProgress] = useState(0);
const progressInterval = useRef<NodeJS.Timeout>();
usePaywall({ placementKey: EPlacementKeys["aura.placement.redesign.main"] });
const { isVideoReady } = usePersonalVideo();
const { createdDate } = useSelector(selectors.selectPersonalVideo);
const handleNext = useCallback(() => {
navigate(routes.client.trialChoiceV1());
}, [navigate]);
useEffect(() => {
if (isVideoReady && progress >= 100) {
handleNext();
}
}, [activeIndexTitle, handleNext, isVideoReady, progress]);
useEffect(() => {
setPeriodClassName("to-nontransparent");
classNameTimeOut.current = setTimeout(() => {
setPeriodClassName("to-transparent");
}, 4000);
if (activeIndexTitle < onboardingTitles.length - 1) {
classNameTimeOut.current = setTimeout(() => {
setPeriodClassName("to-transparent");
}, 4000);
}
titleInterval.current = setTimeout(() => {
if (activeIndexTitle < onboardingTitles.length - 1) {
setIndexTitle((prev) => prev + 1);
} else {
handleNext();
}
}, 5000);
return () => {
@ -40,17 +51,28 @@ function OnboardingPage() {
};
}, [activeIndexTitle, handleNext]);
const getProgressIntervalTiming = useCallback(() => {
const generateTimeLimit = import.meta.env.AURA_PERSONAL_VIDEO_TIME_LIMIT;
if (progress < 95 || isVideoReady) {
return (onboardingTitles.length * 5000) / 100;
}
return (
(Number(generateTimeLimit) * 1000 - new Date().getTime() + createdDate) /
4
);
}, [createdDate, isVideoReady, progress]);
useEffect(() => {
progressInterval.current = setInterval(() => {
setProgress((prev) => {
if (prev >= 100) return prev;
return prev + 1;
});
}, (onboardingTitles.length * 5000) / 100);
}, getProgressIntervalTiming());
return () => {
if (progressInterval.current) clearInterval(progressInterval.current);
};
}, []);
}, [getProgressIntervalTiming]);
return (
<section className={`${styles.page} page`}>

View File

@ -15,8 +15,6 @@ function RelationshipAlmostTherePage() {
preloadKey: ELottieKeys.sun,
});
console.log("animationData", animationData);
const handleBack = () => {
navigate(-1);
};

View File

@ -0,0 +1,60 @@
import React, { useState } from "react";
import ReactPlayer from "react-player";
import styles from "./styles.module.css";
import Loader from "@/components/Loader";
import PlayButton from "../../../../ui/PlayButton";
interface IPersonalVideoProps {
gender: string;
url: string;
}
const PersonalVideo = React.memo<IPersonalVideoProps>(({ url, gender }) => {
const [isPlaying, setIsPlaying] = useState(false);
const [isError, setIsError] = useState(false);
const onError = (error: unknown) => {
if (!error) return;
setIsError(true);
setIsPlaying(false);
};
return (
<div
className={styles.container}
style={{
backgroundColor: gender === "male" ? "#85B6FF" : "#D1ACF2",
}}
>
{!isPlaying && !isError && <Loader className={styles.loader} />}
{isError && (
<PlayButton
backgroundFill={gender === "male" ? "#85B6FF" : "#D1ACF2"}
className={styles["play-button"]}
onClick={() => {
setIsError(false);
setIsPlaying(true);
}}
/>
)}
<ReactPlayer
url={url}
controls={false}
light={false}
muted={false}
playing={isPlaying}
stopOnUnmount={true}
width="100%"
onReady={() => setIsPlaying(true)}
onError={onError}
playsinline={true}
height={"auto"}
style={{
aspectRatio: "16 / 9",
}}
/>
</div>
);
});
export default PersonalVideo;

View File

@ -0,0 +1,14 @@
.container {
width: 100%;
margin: 24px 0 16px;
position: relative;
overflow: hidden;
border-radius: 10px;
}
.play-button, .loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

View File

@ -24,6 +24,7 @@ import { useDynamicSize } from "@/hooks/useDynamicSize";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import PaymentModal from "@/components/PaymentModal";
import PersonalVideo from "./components/PersonalVideo";
function TrialPaymentPage() {
const dispatch = useDispatch();
@ -53,6 +54,7 @@ function TrialPaymentPage() {
"single" | "partner"
>("single");
const { subPlan } = useParams();
const { videoUrl } = useSelector(selectors.selectPersonalVideo);
useEffect(() => {
if (subPlan) {
@ -131,6 +133,7 @@ function TrialPaymentPage() {
/>
<Header className={styles.header} />
<TrialPaymentHeader buttonClick={openStripeModal} />
{!!videoUrl.length && <PersonalVideo gender={gender} url={videoUrl} />}
{singleOrWithPartner === "partner" && (
<WithPartnerInformation
zodiacSign={zodiacSign}

View File

@ -0,0 +1,52 @@
import { SVGProps } from "react";
import styles from "./styles.module.css";
interface IPlayButtonProps {
backgroundFill?: string;
}
function PlayButton({
backgroundFill = "#32BEA6",
className = "",
...props
}: IPlayButtonProps & SVGProps<SVGSVGElement>) {
return (
<>
<svg
height="48px"
width="48px"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="-4.96 -4.96 506.08 506.08"
xmlSpace="preserve"
fill="#000000"
stroke="#000000"
strokeWidth="7.938528000000001"
className={`${styles.svg} ${className}`}
{...props}
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g
id="SVGRepo_tracerCarrier"
strokeLinecap="round"
strokeLinejoin="round"
></g>
<g id="SVGRepo_iconCarrier">
{" "}
<path
style={{ fill: backgroundFill }}
d="M496.158,248.085c0-137.021-111.07-248.082-248.076-248.082C111.07,0.002,0,111.062,0,248.085 c0,137.002,111.07,248.071,248.083,248.071C385.088,496.155,496.158,385.086,496.158,248.085z"
></path>{" "}
<path
style={{ fill: "#FFFFFF" }}
d="M370.805,235.242L195.856,127.818c-4.776-2.934-11.061-3.061-15.951-0.322 c-4.979,2.785-8.071,8.059-8.071,13.762v214c0,5.693,3.083,10.963,8.046,13.752c2.353,1.32,5.024,2.02,7.725,2.02 c2.897,0,5.734-0.797,8.205-2.303l174.947-106.576c4.657-2.836,7.556-7.986,7.565-13.44 C378.332,243.258,375.452,238.096,370.805,235.242z"
></path>{" "}
</g>
</svg>
</>
);
}
export default PlayButton;

View File

@ -0,0 +1,4 @@
.svg {
cursor: pointer;
z-index: 10;
}

3
src/env.d.ts vendored
View File

@ -6,5 +6,6 @@ interface ImportMetaEnv {
AURA_PREFIX: string,
AURA_OPEN_AI_HOST: string,
AURA_OPEN_AI_PREFIX: string,
AURA_YANDEX_COUNTER_NUMBER: string
AURA_YANDEX_COUNTER_NUMBER: string,
AURA_PERSONAL_VIDEO_TIME_LIMIT: string
}

View File

@ -120,7 +120,7 @@ export const useAuthentication = () => {
setIsLoading(true);
setError(null)
const payload = getAuthorizationPayload(email, source);
const { token, userId } = await api.authorization(payload);
const { token, userId, generatingVideo, videoId } = await api.authorization(payload);
const { user } = await api.getUser({ token });
if (userId?.length && !!window.ym && typeof window.ym === 'function') {
window.ym(95799066, 'userParams', {
@ -131,6 +131,8 @@ export const useAuthentication = () => {
}
signUp(token, user);
setToken(token);
dispatch(actions.personalVideo.updateStatus({ generatingVideo: generatingVideo || false, videoId: videoId || "" }));
dispatch(actions.status.update("registred"));
} catch (error) {
setError((error as Error).message);

View File

@ -75,12 +75,9 @@ export const useLottie = ({ preloadKey, loadKey }: IUseLottieProps) => {
useEffect(() => {
if (preloadKey) {
console.log("preload");
preload(preloadKey);
}
if (loadKey) {
console.log("load");
load(loadKey);
}
}, [load, loadKey, preload, preloadKey])

View File

@ -0,0 +1,57 @@
import { useApi } from "@/api";
import { actions, selectors } from "@/store";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
export const usePersonalVideo = () => {
const api = useApi();
const dispatch = useDispatch();
const token = useSelector(selectors.selectToken);
const { generatingVideo, videoId, createdDate } = useSelector(
selectors.selectPersonalVideo
);
const personalVideoTimeLimit = useMemo(() => import.meta.env.AURA_PERSONAL_VIDEO_TIME_LIMIT, [])
const requestVideoInterval = useRef<NodeJS.Timeout>();
const [isVideoReady, setIsVideoReady] = useState(false);
const clearPersonalVideoInterval = useCallback(() => {
if (!requestVideoInterval.current) return;
if (isVideoReady) return clearInterval(requestVideoInterval.current);
}, [isVideoReady])
const getTargetUserVideo = useCallback(async () => {
const videos = await api.getUserVideos({
token,
});
return videos.find((video) => video.id === videoId);
}, [api, token, videoId]);
useEffect(() => {
clearPersonalVideoInterval();
return () => {
clearPersonalVideoInterval();
};
}, [clearPersonalVideoInterval])
useEffect(() => {
if (!generatingVideo) return setIsVideoReady(true);
requestVideoInterval.current = setInterval(async () => {
const video = await getTargetUserVideo();
if (!video) return setIsVideoReady(true);
if (video.videoUrl.length) {
dispatch(actions.personalVideo.updateUrl(video.videoUrl));
return setIsVideoReady(true);
}
if (new Date().getTime() - createdDate > Number(personalVideoTimeLimit) * 1000) {
return setIsVideoReady(true);
}
}, 5000);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return useMemo(() => ({ isVideoReady }), [isVideoReady])
}

View File

@ -274,6 +274,10 @@ const routes = {
makePayment: () =>
[dApiHost, dApiPrefix, "payment", "checkout"].join("/"),
// User videos
getUserVideos: () =>
[dApiHost, "users", "videos", "combined"].join("/"),
},
openAi: {
createThread: () => [openAIHost, openAiPrefix, "threads"].join("/"),

View File

@ -72,6 +72,7 @@ import palmistry, {
} from "./palmistry";
import { selectPaywallsIsMustUpdate, selectPaywalls } from "./paywalls";
import privacyPolicy, { actions as privacyPolicyActions, selectPrivacyPolicy } from "./privacyPolicy";
import personalVideo, { actions as personalVideoActions, selectPersonalVideo } from "./personalVideo";
const preloadedState = loadStore();
export const actions = {
@ -92,6 +93,7 @@ export const actions = {
userConfig: userConfigActions,
palmistry: palmistryActions,
privacyPolicy: privacyPolicyActions,
personalVideo: personalVideoActions,
reset: createAction("reset"),
};
export const selectors = {
@ -127,6 +129,7 @@ export const selectors = {
selectPaywallsIsMustUpdate,
selectPrivacyPolicy,
selectStripeButton,
selectPersonalVideo,
...formSelectors,
};
@ -147,7 +150,8 @@ export const reducer = combineReducers({
userConfig,
palmistry,
paywalls,
privacyPolicy
privacyPolicy,
personalVideo
});
export type RootState = ReturnType<typeof reducer>;

View File

@ -0,0 +1,37 @@
import { createSlice, createSelector } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
interface IPersonalVideo {
generatingVideo: boolean
videoId: string
videoUrl: string
createdDate: number
}
const initialState: IPersonalVideo = {
generatingVideo: false,
videoId: "",
videoUrl: "",
createdDate: 0
}
const personalVideoSlice = createSlice({
name: 'personalVideo',
initialState,
reducers: {
updateStatus(state, action: PayloadAction<Omit<IPersonalVideo, 'createdDate' | 'videoUrl'>>) {
return { ...state, ...action.payload, createdDate: new Date().getTime() }
},
updateUrl(state, action: PayloadAction<string>) {
return { ...state, videoUrl: action.payload }
}
},
extraReducers: (builder) => builder.addCase('reset', () => initialState),
})
export const { actions } = personalVideoSlice
export const selectPersonalVideo = createSelector(
(state: { personalVideo: IPersonalVideo }) => state.personalVideo,
(personalVideo) => personalVideo
)
export default personalVideoSlice.reducer