בתום מדריך זה, תהיה לנו אפליקציית ניהול משאבי אנוש שכוללת:
עמוד כניסה: מאפשר למשתמשים להיכנס כמנהל או כעובד. למנהלים יש גישה לעמודי חופשה ובקשות, בעוד שעובדים יש להם גישה רק לעמוד חופשה.
עמודי חופשה: מאפשרים לעובדים לבקש, לראות ולבטל את החופשות שלהם. כמו כן, מנהלים יכולים להקצות חופשות חדשות.
עמוד בקשות: נגיש רק למנהלי משאבי אנוש לאישור או דחיית בקשות חופשה.
הערה: ניתן לקבל את קוד המקור המלא של האפליקציה שנבנה במדריך זה מהמאגר GitHub הזה
בעת ביצוע אלה, נשתמש ב:
ממשק API: כדי לאחזר ולעדכן את הנתונים. לרפין יש חבילות ספקי נתונים וממשקי REST מובנים, אך אתה יכול גם לבנות את שלך כדי להתאים לדרישות הספציפיות שלך. במדריך הזה, אנו הולכים להשתמש בNestJs CRUD כשירות צד אחורי שלנו ובחבילה @refinedev/nestjsx-crud כספק הנתונים שלנו.
Material UI: נשתמש בזה עבור רכיבי UI וניצור התאמה אישית מלאה בהתאם לעיצוב שלנו. לרפין יש תמיכה מובנית עבור Material UI, אך אתה יכול להשתמש בכל ספריית UI שתרצה.
ברגע שנבנה את האפליקציה, נשים אותה באינטרנט באמצעות DigitalOcean’s App Platform שמקלה על ההקמה, השקה וצמיחה של אפליקציות ואתרים סטטיים. תוכל להפעיל קוד פשוט על ידי הפניה למאגר GitHub ולתת לפלטפורמת האפליקציה לבצע את העבודות הקשות של ניהול התשתית, ריצות האפליקציה ותלויות.
Refine הוא מסגרת מטה-React קוד פתוח לבניית יישומי אינטרנט B2B מורכבים, בעיקר מקרים של ניהול נתונים כמו כלים פנימיים, לוחות בקרה ולוחות ניהול. הוא מעוצב על מנת לספק סט של הוקים ורכיבים כדי לשפר את תהליך הפיתוח עם זרימת עבודה טובה יותר למפתח.
הוא מספק תכונות משלימות, מוכנות לייצור עבור אפליקציות ברמת ארגון כדי לפשט משימות בתשלום כמו ניהול מצב ונתונים, אימות, ובקרת גישה. זה מאפשר למפתחים להישאר ממוקדים בליבת האפליקציה שלהם בצורה שמתאימה ממספר פרטי יישום מכבידים.
נשתמש בפקודת npm create refine-app כדי לאתחל את הפרויקט באופן אינטראקטיבי.
npm create refine-app@latest
בחר את האפשרויות הבאות כאשר תתבקש:
✔ Choose a project template · Vite
✔ What would you like to name your project?: · hr-app
✔ Choose your backend service to connect: · nestjsx-crud
✔ Do you want to use a UI Framework?: · Material UI
✔ Do you want to add example pages?: · No
✔ Do you need any Authentication logic?: · None
✔ Choose a package manager: · npm
לאחר שההתקנה הושלמה, נווט לתיקיית הפרויקט והתחל את האפליקציה שלך עם:
npm run dev
פתח את http://localhost:5173 בדפדפן שלך כדי לראות את האפליקציה.
עכשיו כשיש לנו את הפרויקט מוכן, בואו נעשה כמה שינויים במבנה הפרויקט ונשמור על הקבצים הלא נחוצים.
ראשית, התקן את התלויות של צד שלישי:
@mui/x-date-pickers, @mui/x-date-pickers-pro: אלו הם רכיבי בחירת תאריך עבור Material UI. נשתמש בהם כדי לבחור את טווח התאריכים לבקשות חופשה.
react-hot-toast: ספריית טוסט מינימליסטית עבור React. נשתמש בה כדי להציג הודעות הצלחה ושגיאה.
react-infinite-scroll-component: רכיב React שמקל על גלילה אינסופית. נשתמש בו כדי לטעון עוד בקשות חופשה כאשר המשתמש גולל למטה בדף כדי לראות בקשות נוספות.
dayjs: ספריית תאריכים קלה לעיבוד, אימות, מניפולציה ועיצוב תאריכים.
vite-tsconfig-paths: תוסף Vite שמאפשר לך להשתמש באליאסים של נתיב TypeScript בפרויקט Vite שלך.
לאחר התקנת התלויות, עדכן את vite.config.ts ואת tsconfig.json כדי להשתמש בתוסף vite-tsconfig-paths. זה מאפשר קיצורי דרך של TypeScript בפרויקטי Vite, ומאפשר ייבוא עם הקיצור @.
src/contexts: התיקיה הזו מכילה קובץ אחד שהוא ColorModeContext. הוא מטפל במצב כהה/בהיר עבור האפליקציה. לא נשתמש בו במדריך הזה.
src/components: התיקיה הזו מכילה את הרכיב <Header />. נשתמש ברכיב כותרת מותאם אישית במדריך הזה.
rm-rf src/contexts src/components
לאחר הסרת הקבצים והתקיות, App.tsx נותן שגיאה אותה נתקן בשלב הבא.
במהלך המדריך, נעסוק בקידוד הדפים והרכיבים המרכזיים. אז, קח את הקבצים והתקיות הנדרשות מהמאגר GitHub. עם קבצים אלה, תהיה לנו מבנה בסיסי עבור אפליקציית ניהול משאבי אנוש שלנו.
אייקונים: תיקיית אייקונים המכילה את כל האייקונים של האפליקציה.
משאבים: מערך המפרט את ישויות הנתונים (employee ו-manager) ש-Refine ישיג. אנו משתמשים במשאבים הורים וילדים כדי לארגן נתונים ולנהל הרשאות. לכל משאב יש scope המגדיר את תפקיד המשתמש, אשר שולט בגישה לחלקים שונים של האפליקציה.
queryClient: לקוח שאילתות מותאם אישית לשליטה מלאה והתאמה אישית של אחזור הנתונים.
syncWithLocation: מאפשר סנכרון של מצב האפליקציה (מניחים, מסננים, פייג'ינציה וכו') עם ה-URL.
הסתכל מקרוב על הtheme-provider. התאמתנו במידה רבה את נושא Material UI כך שיתאים לעיצוב של אפליקציית ניהול משאבי אנוש, ויצרנו שני נושאים – אחד למנהלים ואחד לעובדים כדי להבחין ביניהם בצבעים שונים.
בנוסף, הוספנו את Inter כגופן מותאם אישית לאפליקציה. כדי להתקין יש להוסיף את השורה הבאה לקובץ index.html:
<head><metacharset="utf-8"/><linkrel="icon"href="/favicon.ico"/><^><link<^/><^>
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
rel="stylesheet" /> <^><metaname="viewport"content="width=device-width, initial-scale=1"/><metaname="theme-color"content="#000000"/><metaname="description"content="refine | Build your React-based CRUD applications, without constraints."/><metadata-rh="true"property="og:image"content="https://refine.dev/img/refine_social.png"/><metadata-rh="true"name="twitter:image"content="https://refine.dev/img/refine_social.png"/><title>
Refine - Build your React-based CRUD applications, without constraints.
</title></head>
בשלב הקודם הוספנו רכיב פריסה מותאם אישית לאפליקציה. לרוב, ניתן להשתמש בפריסת ברירת המחדל של מרכז הממשק המשתמש אך רצינו להראות כיצד ניתן לבצע התאמה אישית.
רכיב הפריסה מכיל את הכותרת, סרגל הצד ואזור התוכן הראשי. הוא משתמש ב־<פריסתערכהV2 /> כבסיס ומותאם אותו כך שיתאים לעיצוב של אפליקציית ניהול משאבי אנוש.
הסרגל הצדדי מכיל את לוגו האפליקציה וקישורי ניווט. במכשירים ניידים זהו סרגל צדדי מתכווץ שנפתח כאשר המשתמש לוחץ על אייקון התפריט. קישורי הניווט מוכנים עם useMenu מה-hook של Refine ומוצגים בהתאם לתפקיד המשתמש בעזרת <CanAccess /> רכיב.
המותקן על הסרגל הצדדי, מציג את האוואטר ושם המשתמש המחובר. כאשר לוחצים עליו, נפתח פופובר עם פרטי המשתמש וכפתור התנתקות. משתמשים יכולים לעבור בין תפקידים שונים על ידי בחירה מתוך התפריט הנפתח. רכיב זה מאפשר בדיקה על ידי מעבר בין משתמשים עם תפקידים שונים.
הוא לא מציג דבר במכשירים שולחניים. במכשירים ניידים, הוא מציג את לוגו האפליקציה ואייקון תפריט לפתיחת הסרגל הצדדי. הכותרת דביקה ותמיד נראית בחלק העליון של העמוד.
זה מציג את כותרת הדף וכפתורי ניווט. כותרת הדף מיוצרת אוטומטית עם useResource הוק, אשר שולף את שם המשאב מההקשר של Refine. זה מאפשר לנו לשתף את אותו עיצוב ופריסה בכל האפליקציה.
בשלב זה, ניישם את הלוגיקה של אימות והרשאות עבור אפליקציית ניהול משאבי אנוש שלנו. זה יהיה דוגמה מצוינת לשליטת גישה באפליקציות ארגוניות.
כאשר משתמשים נכנסים כמנהלים, הם יוכלו לראות את דפי Time Off ו־Requests. אם הם נכנסים כעובדים, הם יראו רק את דף Time Off. מנהלים יכולים לאשר או לדחות בקשות חופשה בדף Requests.
עובדים יכולים לבקש חופשה ולצפות בהיסטוריה שלהם בדף חופשה. כדי ליישם זאת, אנו נשתמש בתכונות authProvider ו-accessControlProvider של Refine.
אימות
ב-Refine, האימות מתבצע על ידי ה-authProvider. זה מאפשר לך להגדיר את לוגיקת האימות עבור האפליקציה שלך. בשלב הקודם, כבר העתקנו את ה-authProvider מהמאגרים של GitHub והענקנו לו את הרכיב <Refine /> כפרופס. נשתמש בהוקס ורכיבים הבאים כדי לשלוט בהתנהגות האפליקציה שלנו בהתאם אם המשתמש מחובר או לא.
useLogin: הוק שמספק פונקציית mutate כדי להיכנס למשתמש.
useLogout: הוק שמספק פונקציית mutate כדי להתנתק מהמשתמש.
ב-Refine, האישור מנוהל על ידי הaccessControlProvider. הוא מאפשר לך להגדיר תפקידים והרשאות למשתמשים, ולשלוט בגישה לחלקים שונים של האפליקציה בהתאם לתפקיד המשתמש. בשלב הקודם, כבר העברנו את הaccessControlProvider ממאגר ה-GitHub ונתנו אותו לרכיב <Refine /> כפרופ. בואו נסתכל מקרוב על הaccessControlProvider כדי לראות כיצד הוא עובד.
src/providers/access-control/index.ts
import type {AccessControlBindings}from"@refinedev/core";import{Role}from"@/types";exportconstaccessControlProvider:AccessControlBindings={options:{queryOptions:{keepPreviousData:true,},buttons:{hideIfUnauthorized:true,},},can:async({ params, action })=>{const user =JSON.parse(localStorage.getItem("user")||"{}");if(!user)return{can:false};const scope = params?.resource?.meta?.scope;// אם למשאב אין טווח, הוא אינו נגישif(!scope)return{can:false};if(user.role===Role.MANAGER){return{can:true,};}if(action ==="manager"){return{can: user.role===Role.MANAGER,};}if(action ==="employee"){return{can: user.role===Role.EMPLOYEE,};}// משתמשים יכולים לגשת למשאבים רק אם התפקיד שלהם תואם את טווח המשאבreturn{can: user.role=== scope,};},};
באפליקציה שלנו, יש לנו שני תפקידים: MANAGER וEMPLOYEE.
מנהלים יש גישה לדף Requests, בעוד שעובדים רק יש גישה לדף Time Off. מפעיל ה-accessControlProvider בודק את תפקיד המשתמש ואת תחום המשאב כדי לקבוע האם המשתמש יכול לגשת למשאב. אם תפקיד המשתמש תואם לתחום המשאב, הם יכולים לגשת למשאב. אחרת, הם נדחים מגישה. נשתמש ב-useCan הוק וברכיב <CanAccess /> כדי לשלוט בהתנהגות האפליקציה שלנו בהתבסס על תפקיד המשתמש.
בשלב הקודם, הוספנו את ה-authProvider לרכיב <Refine />. ה-authProvider אחראי על טיפול באימות.
ראשית, אנו צריכים לקבל תמונות. נשתמש בתמונות אלו כתמונות רקע עבור עמוד ההתחברות. ניצור תיקייה חדשה בשם images בתיקיית public ונקבל את התמונות מתוך מאגר הקוד ב-GitHub.
לאחר קבלת התמונות, בואו ניצור קובץ חדש בשם index.tsx בתיקיית src/pages/login ונוסיף את הקוד הבא:
כדי לפשט את תהליך האימות, יצרנו אובייקט mockUsers עם שני מערכים: managers ו-employees. כל מערך מכיל אובייקטי משתמש מוגדרים מראש. כאשר משתמש בוחר אימייל מתוך הרשימה הנפתחת ולוחץ על כפתור Sign in, הפונקציה login נקראת עם האימייל שנבחר. הפונקציה login היא פונקציית מוטציה המסופקת על ידי ההוק useLogin מ-Refine. היא קוראת לauthProvider.login עם האימייל שנבחר.
לאחר מכן, בואו נייבא את רכיב <PageLogin /> ונעדכן את קובץ App.tsx עם השינויים המודגשים.
בקובץ המעודכן App.tsx, הוספנו את רכיב <Authenticated /> מ-Refine. רכיב זה משמש להגן על מסלולים שדורשים אימות. הוא מקבל פרופ key כדי לזהות את הרכיב באופן ייחודי, פרופ fallback לה-render כאשר המשתמש אינו מאומת, ופרופ redirectOnFail להפנות את המשתמש למסלול המיועד כאשר האימות נכשל. מתחת למכסה המנוע הוא קורא למתודה authProvider.check כדי לבדוק אם המשתמש מאומת.
<Authenticated /> רכיב המקיף את הנתיב path="*" כדי לבדוק את מעמד האימות של המשתמש. נתיב זה הוא נתיב תפיסה כללית שמציג את רכיב ה־<ErrorComponent /> כאשר המשתמש מאומת. זה מאפשר לנו להציג עמוד 404 כאשר המשתמש מנסה לגשת לנתיב שאינו קיים.
כעת, כאשר אתה מפעיל את היישום ועובר לנתיב http://localhost:5173/login, אתה צריך לראות את עמוד הכניסה עם הרשימה הנפתחת לבחירת המשתמש.
כרגע, הדף "/" לא עושה כלום. בשלבים הבאים נממש את עמודי Time Off ו־Requests.
בשלב זה, נבנה את דף ה-חופשות. עובדים יכולים לבקש חופש ולראות את היסטוריית החופשות שלהם. מנהלים גם יכולים לצפות בהיסטוריה שלהם, אך במקום לבקש חופש, הם יכולים להקצות אותו לעצמם ישירות. נבצע זאת באמצעות accessControlProvider, רכיב ה-<CanAccess /> וה-useCan hook.
<PageEmployeeTimeOffsList />
לפני שנתחיל לבנות את דף החופשות, עלינו ליצור כמה רכיבים להצגת היסטוריית חופשות, בקשות חופשות עתידיות וסטטיסטיקות של חופשות שנעשה שימוש בהן. בסיום שלב זה, נשתמש ברכיבים אלו כדי לבנות את דף החופשות.
בניית רכיב <TimeOffList /> כדי להציג את היסטוריית החופשות
צור תיקייה חדשה בשם time-offs בתוך התיקייה src/components. בתוך התיקייה time-offs, צור קובץ חדש בשם list.tsx והוסף את הקוד הבא:
<DateField />: מעצב ומציג תאריכים בצורה ידידותית למשתמש.
value: התאריך להצגה.
format: מגדיר את פורמט התאריך (למשל, "5 בינואר").
5. יצירת מסננים ומסדרים על בסיס type
מסננים:
constfilters:Record<Props["type"],CrudFilters>={history:[{field:"status",operator:"eq",value:TimeOffStatus.APPROVED,},{field:"endsAt",operator:"lt",value: today,},],// ... סוגים אחרים};
מגדיר קריטריונים לשליפת ימי חופש על בסיס מצב ותאריכים.
history: שולף ימי חופש מאושרים שכבר הסתיימו.
upcoming: שולף ימי חופש מאושרים שהולכים להתקיים.
מסדרים:
constsorters:Record<Props["type"],CrudSort[]>={history:[{field:"startsAt",order:"desc"}],// ... סוגים אחרים};
קובע את הסדר של הנתונים שנשלפים.
history: ממיין לפי תאריך התחלה בסדר יורד.
בניית רכיב <TimeOffLeaveCards /> כדי להציג סטטיסטיקות של ימי חופש שנוצלו.
צור קובץ חדש בשם leave-cards.tsx בתיקיית src/components/time-offs והוסף את הקוד הבא:
src/components/time-offs/leave-cards.tsx
import{ useGetIdentity, useList }from"@refinedev/core";import{Box,Grid,Skeleton,Typography}from"@mui/material";import{AnnualLeaveIcon,CasualLeaveIcon,SickLeaveIcon}from"@/icons";import{
type Employee,TimeOffStatus,TimeOffType,
type TimeOff,}from"@/types";
type Props={
employeeId?: number;};exportconstTimeOffLeaveCards=(props:Props)=>{const{data: employee,isLoading: isLoadingEmployee }=
useGetIdentity<Employee>({queryOptions:{enabled:!props.employeeId,},});
const {data: timeOffsSick,isLoading: isLoadingTimeOffsSick } =
useList<TimeOff>({resource:"time-offs",// אנו זקוקים רק לסך כל ימי המחלה, לכן נוכל לקבוע את pageSize ל-1 כדי להפחית את העומסpagination:{pageSize:1},filters:[{field:"status",operator:"eq",value:TimeOffStatus.APPROVED,},{field:"timeOffType",operator:"eq",value:TimeOffType.SICK,},{field:"employeeId",operator:"eq",value: employee?.id,},],queryOptions:{enabled:!!employee?.id,},});
const {data: timeOffsCasual,isLoading: isLoadingTimeOffsCasual } =
useList<TimeOff>({resource:"time-offs",// אנו זקוקים רק לסך כל ימי המחלה, לכן נוכל לקבוע את pageSize ל-1 כדי להפחית את העומסpagination:{pageSize:1},filters:[{field:"status",operator:"eq",value:TimeOffStatus.APPROVED,},{field:"timeOffType",operator:"eq",value:TimeOffType.CASUAL,},{field:"employeeId",operator:"eq",value: employee?.id,},],queryOptions:{enabled:!!employee?.id,},});
const loading =
isLoadingEmployee || isLoadingTimeOffsSick || isLoadingTimeOffsCasual;
return (
<Gridcontainerspacing="24px"><Griditemxs={12}sm={4}><Cardloading={loading}type="annual"value={employee?.availableAnnualLeaveDays ||0}/></Grid><Griditemxs={12}sm={4}><Cardloading={loading}type="sick"value={timeOffsSick?.total ||0}/></Grid><Griditemxs={12}sm={4}><Cardloading={loading}type="casual"value={timeOffsCasual?.total ||0}/></Grid></Grid>
);
};
const variantMap = {annual:{label:"Annual Leave",description:"Days available",bgColor:"primary.50",titleColor:"primary.900",descriptionColor:"primary.700",iconColor:"primary.700",icon:<AnnualLeaveIcon/>,},sick:{label:"Sick Leave",description:"Days used",bgColor:"#FFF7ED",titleColor:"#7C2D12",descriptionColor:"#C2410C",iconColor:"#C2410C",icon:<SickLeaveIcon/>,},casual:{label:"Casual Leave",description:"Days used",bgColor:"grey.50",titleColor:"grey.900",descriptionColor:"grey.700",iconColor:"grey.700",icon:<CasualLeaveIcon/>,},};
const Card = (props: {type:"annual"|"sick"|"casual";value: number;
loading?: boolean;}) => {return(<Boxsx={{backgroundColor: variantMap[props.type].bgColor,padding:"24px",borderRadius:"12px",}}><Boxsx={{display:"flex",alignItems:"center",justifyContent:"space-between",}}><Typographyvariant="h6"sx={{color: variantMap[props.type].titleColor,fontSize:"16px",fontWeight:500,lineHeight:"24px",}}>{variantMap[props.type].label}</Typography><Boxsx={{color: variantMap[props.type].iconColor,}}>{variantMap[props.type].icon}</Box></Box><Boxsx={{marginTop:"8px",display:"flex",flexDirection:"column"}}>{props.loading?(<Boxsx={{width:"40%",height:"32px",display:"flex",alignItems:"center",justifyContent:"center",}}><Skeletonvariant="rounded"sx={{width:"100%",height:"20px",}}/></Box>):(<Typographyvariant="caption"sx={{color: variantMap[props.type].descriptionColor,fontSize:"24px",lineHeight:"32px",fontWeight:600,}}>{props.value}</Typography>)}<Typographyvariant="body1"sx={{color: variantMap[props.type].descriptionColor,fontSize:"12px",lineHeight:"16px",}}>{variantMap[props.type].description}</Typography></Box></Box>);};
<TimeOffLeaveCards />
המרכיב <TimeOffLeaveCards /> מציג סטטיסטיקות על חופשות של עובד. הוא מציג שלוש כרטיסים עבור חופשה שנתית, חופשת מחלה וחופשה רגילה, ומראה כמה ימים זמינים או מנוצלים.
בואו נפרק את החלקים המרכזיים של המרכיב:
1. שליפת נתונים
נתוני עובד: משתמש ב-useGetIdentity כדי לקבל את המידע על העובד הנוכחי, כמו ימי חופשה שנתית זמינים.
ספירת חופשות: משתמש ב-useList כדי לשלוף את סך כל ימי המחלה וחופשות רגילות שנוצלו על ידי העובד. הוא קובע את pageSize ל-1 כי אנו זקוקים רק לסך הכל, ולא לכל הפרטים.
2. הצגת הכרטיסים
המרכיב מצייר שלושה כרטיסים, אחד לכל סוג חופשה.
כל כרטיס מראה:
סוג החופשה (למשל, חופשה שנתית).
מספר הימים הזמינים או המנוצלים.
אייקון המייצג את סוג החופשה.
3. טיפול במצבי טעינה
אם הנתונים עדיין נטענים, מוצג מקום מחזיק גוף במקום המספרים האמיתיים.
המאפיין loading מועבר לכרטיסים כדי לנהל מצב זה.
4. רכיב הכרטיס
מקבל את type, value, ו-loading כמאפיינים.
משתמש ב-variantMap כדי לקבל את התוויות, הצבעים והאיקונים הנכונים בהתאם לסוג החופשה.
מציג את המידע על החופשה עם עיצוב מתאים.
בניית <PageEmployeeTimeOffsList />
עכשיו שיש לנו את הרכיבים לרשימת החופשות והצגת כרטיסי החופשה, ניצור את הקובץ החדש בתיקייה src/pages/employee/time-offs/ בשם list.tsx ונוסיף את הקוד הבא:
src/pages/time-off.tsx
import{CanAccess, useCan }from"@refinedev/core";import{CreateButton}from"@refinedev/mui";import{Box,Grid}from"@mui/material";import{PageHeader}from"@/components/layout/page-header";import{TimeOffList}from"@/components/time-offs/list";import{TimeOffLeaveCards}from"@/components/time-offs/leave-cards";import{TimeOffIcon}from"@/icons";import{ThemeProvider}from"@/providers/theme-provider";import{Role}from"@/types";exportconstPageEmployeeTimeOffsList=()=>{const{data: useCanData }=useCan({action:"manager",params:{resource:{name:"time-offs",meta:{scope:"manager",},},},});const isManager = useCanData?.can;return(<ThemeProviderrole={isManager ?Role.MANAGER:Role.EMPLOYEE}><Box><PageHeadertitle="Time Off"rightSlot={<CreateButtonsize="large"variant="contained"startIcon={<TimeOffIcon/>}><CanAccessaction="manager"fallback="Request Time Off">
Assign Time Off
</CanAccess></CreateButton>}/><TimeOffLeaveCards/><Gridcontainerspacing="24px"sx={{marginTop:"24px",}}><Griditemxs={12}md={6}><Boxsx={{display:"flex",flexDirection:"column",gap:"24px",}}><TimeOffListtype="inReview"/><TimeOffListtype="upcoming"/></Box></Grid><Griditemxs={12}md={6}><TimeOffListtype="history"/></Grid></Grid></Box></ThemeProvider>);};
<PageEmployeeTimeOffsList /> הוא הרכיב הראשי עבור עמוד החופשות, נשתמש ברכיב זה כדי להציג את רשימות החופשות וכרטיסי החופשה כאשר המשתמשים ניגשים לנתיב /employee/time-offs.
<PageEmployeeTimeOffsList />
בואו נפרק את החלקים המרכזיים של הרכיב:
1. בדיקת תפקידי המשתמש
משתמש ב-useCan כדי לקבוע אם המשתמש הנוכחי הוא מנהל.
מגדיר את isManager ל-true אם למשתמש יש הרשאות מנהל.
2. יישום נושא בהתאם לתפקיד
מעטפת את התוכן בתוך <ThemeProvider />.
השינוי בתבנית מתבסס על האם המשתמש הוא מנהל או עובד.
3. כותרת הדף עם כפתור מותנה
מציגה <PageHeader /> עם הכותרת "חופשה".
כוללת <CreateButton /> שמשתנה בהתאם לתפקיד המשתמש:
אנחנו משתמשים ב-<NavigateToResource /> כדי להפנות משתמשים למשאב time-offs כאשר הם ניגשים לנתיב /. זה מבטיח שמשתמשים רואים את רשימת ה-time-off כברירת מחדל.
3. הפניית משתמשים למשאב "time-offs" כאשר הם מזוהים
אנחנו מארגנים את דפי העובדים באמצעות נתיבים מקוננים. קודם כל, אנחנו יוצרים נתיב ראשי עם path='employee' שמעטיף תוכן בעיצוב וסגנון ספציפיים לעובד. בתוך נתיב זה, אנחנו מוסיפים path='time-offs', שמציג את רכיב PageEmployeeTimeOffsList. המבנה הזה מקבץ את כל הפיצ'רים של העובדים תחת נתיב אחד ושומר על אחידות בעיצוב.
לאחר שהוספנו את השינויים הללו, אתה יכול לנווט לנתיב /employee/time-offs כדי לראות את עמוד רשימת ה-time offs בפעולה.
/employee/time-offs
כרגע, עמוד רשימת ה-time offs פונקציונלי, אבל חסרה לו היכולת ליצור בקשות חדשות לחופשה. בואו נוסיף את היכולת ליצור בקשות חדשות לחופשה.
ניצור דף חדש לבקשה או הקצאה של חופשה. דף זה יכלול טופס שבו המשתמשים יוכלו לציין את סוג החופשה, תאריכי התחלה וסיום, וכן הערות נוספות.
לפני שנתחיל, עלינו ליצור רכיבים חדשים לשימוש בטופס:
בניית רכיב <TimeOffFormSummary />
צור קובץ חדש בשם form-summary.tsx בתיקיית src/components/time-offs/ והוסף את הקוד הבא:
src/components/time-offs/form-summary.tsx
import{Box,Divider,Typography}from"@mui/material";
type Props={availableAnnualDays: number;requestedDays: number;};exportconstTimeOffFormSummary=(props:Props)=>{const remainingDays = props.availableAnnualDays- props.requestedDays;return(<Boxsx={{display:"flex",flexDirection:"column",alignItems:"flex-end",gap:"16px",whiteSpace:"nowrap",}}><Boxsx={{display:"flex",gap:"16px",}}><Typographyvariant="body2"color="text.secondary">
Available Annual Leave Days:
</Typography><Typographyvariant="body2">{props.availableAnnualDays}</Typography></Box><Boxsx={{display:"flex",gap:"16px",}}><Typographyvariant="body2"color="text.secondary">
Requested Days:
</Typography><Typographyvariant="body2">{props.requestedDays}</Typography></Box><Dividersx={{width:"100%",}}/><Boxsx={{display:"flex",gap:"16px",height:"40px",}}><Typographyvariant="body2"color="text.secondary">
Remaining Days:
</Typography><Typographyvariant="body2"fontWeight={500}>{remainingDays}</Typography></Box></Box>);};
<TimeOffFormSummary />
הרכיב <TimeOffFormSummary /> מציג סיכום של בקשת החופשה. הוא מציג את ימי החופשה השנתיים הזמינים, את מספר הימים המבוקשים ואת הימים שנותרו. נשתמש ברכיב זה בטופס החופשה כדי לספק למשתמשים תמונה ברורה של הבקשה שלהם.
בניית רכיב <PageEmployeeTimeOffsCreate />
צור קובץ חדש בשם create.tsx בתיקיית src/pages/employee/time-offs/ והוסף את הקוד הבא:
src/pages/time-offs/create.tsx
import{ useCan, useGetIdentity, type HttpError}from"@refinedev/core";import{ useForm }from"@refinedev/react-hook-form";import{Controller}from"react-hook-form";import type {DateRange}from"@mui/x-date-pickers-pro/models";import{Box,Button,MenuItem,Select,Typography}from"@mui/material";importdayjsfrom"dayjs";import{PageHeader}from"@/components/layout/page-header";import{InputText}from"@/components/input/text";import{LoadingOverlay}from"@/components/loading-overlay";import{InputDateStartsEnds}from"@/components/input/date-starts-ends";import{TimeOffFormSummary}from"@/components/time-offs/form-summary";import{ThemeProvider}from"@/providers/theme-provider";import{
type Employee,
type TimeOff,TimeOffType,TimeOffStatus,Role,}from"@/types";import{CheckRectangleIcon}from"@/icons";
type FormValues=Omit<TimeOff,"id"|"notes">&{notes: string;dates:DateRange<dayjs.Dayjs>;
};
export const PageEmployeeTimeOffsCreate = () => {const{data: useCanData }=useCan({action:"manager",params:{resource:{name:"time-offs",meta:{scope:"manager",},},},});const isManager = useCanData?.can;const{data: employee }=
useGetIdentity<Employee>();
const {refineCore:{ formLoading, onFinish },...formMethods
} = useForm<TimeOff, HttpError, FormValues>({defaultValues:{timeOffType:TimeOffType.ANNUAL,notes:"",dates:[null,null],},refineCoreProps:{successNotification:()=>{return{message: isManager
?"Time off assigned":"Your time off request is submitted for review.",type:"success",};},},});
const { control, handleSubmit, formState, watch } = formMethods;
const onFinishHandler = async (values: FormValues) => {constpayload:FormValues={...values,startsAt:dayjs(values.dates[0]).format("YYYY-MM-DD"),endsAt:dayjs(values.dates[1]).format("YYYY-MM-DD"),...(isManager &&{status:TimeOffStatus.APPROVED,}),};awaitonFinish(payload);};
const timeOffType = watch("timeOffType");
const selectedDays = watch("dates");
const startsAt = selectedDays[0];
const endsAt = selectedDays[1];
const availableAnnualDays = employee?.availableAnnualLeaveDays ?? 0;
const requestedDays =
startsAt && endsAt ? endsAt.diff(startsAt, "day") + 1 : 0;
return (
<ThemeProviderrole={isManager ?Role.MANAGER:Role.EMPLOYEE}><LoadingOverlayloading={formLoading}><Box><PageHeadertitle={isManager ?"Assign Time Off":"Request Time Off"}showListButtonshowDivider/><Boxcomponent="form"onSubmit={handleSubmit(onFinishHandler)}sx={{display:"flex",flexDirection:"column",gap:"24px",marginTop:"24px",}}><Box><Typographyvariant="body2"sx={{mb:"8px",}}>
Time Off Type
</Typography>
<Controller
name="timeOffType"
control={control}
render={({ field })=>(<Select{...field}size="small"sx={{minWidth:"240px",height:"40px","& .MuiSelect-select":{paddingBlock:"10px",},}}><MenuItemvalue={TimeOffType.ANNUAL}>Annual Leave</MenuItem><MenuItemvalue={TimeOffType.CASUAL}>Casual Leave</MenuItem><MenuItemvalue={TimeOffType.SICK}>Sick Leave</MenuItem></Select>)}
/>
</Box><Box><Typographyvariant="body2"sx={{mb:"16px",}}>
Requested Dates
</Typography>
<Controller
name="dates"
control={control}
rules={{validate:(value)=>{if(!value[0]||!value[1]){return"Please select both start and end dates";}returntrue;},}}
render={({ field })=>{return(<Box
sx={{display:"grid",gridTemplateColumns:()=>{return{sm:"1fr",lg:"628px 1fr",};},gap:"40px",}}><InputDateStartsEnds{...field}error={formState.errors.dates?.message}availableAnnualDays={availableAnnualDays}requestedDays={requestedDays}/>{timeOffType ===TimeOffType.ANNUAL&&(<Box
sx={{display:"flex",maxWidth:"628px",alignItems:()=>{return{lg:"flex-end",};},justifyContent:()=>{return{xs:"flex-end",lg:"flex-start",};},}}><TimeOffFormSummaryavailableAnnualDays={availableAnnualDays}requestedDays={requestedDays}/></Box>
)}
</Box>
);
}}
/>
</Box><Boxsx={{maxWidth:"628px",}}><Controllername="notes"control={control}render={({ field, fieldState })=>{return(<InputText{...field}label="Notes"error={fieldState.error?.message}placeholder="Place enter your notes"multilinerows={3}/>);}}/></Box><Buttonvariant="contained"size="large"type="submit"startIcon={isManager ?<CheckRectangleIcon/>:undefined}>{isManager ?"Assign":"Send Request"}</Button></Box></Box></LoadingOverlay></ThemeProvider>
);
};
<PageEmployeeTimeOffsCreate />
הרכיב <PageEmployeeTimeOffsCreate /> מציג טופס ליצירת בקשות חופשה חדשות באפליקציית ניהול משאבי אנוש. גם עובדים וגם מנהלים יכולים להשתמש בו כדי לבקש או להקצות חופשה. הטופס כולל אפשרויות לבחירת סוג החופשה, לבחור תאריכי התחלה וסיום, להוסיף הערות, והוא מציג סיכום של החופשה המבוקשת.
עם הַפֵּסֶק useCan, אנחנו בודקים אם המשתמש הנוכחי מורשה כמנהל. זה קובע אם המשתמש יכול להקצות זמן פנוי או רק לבקש אותו. אנחנו נתמוך בהגשת הטופס באופן שונה ב-onFinishHandler בהתאם לתפקיד המשתמש.
2. מצב והגשת הטופס
const{refineCore:{ formLoading, onFinish },...formMethods
}= useForm<TimeOff,HttpError,FormValues>({defaultValues:{timeOffType:TimeOffType.ANNUAL,notes:"",dates:[null,null],},refineCoreProps:{successNotification:()=>{return{message: isManager
?"Time off assigned":"Your time off request is submitted for review.",type:"success",};},},});const{ control, handleSubmit, formState, watch }= formMethods;constonFinishHandler=async(values:FormValues)=>{constpayload:FormValues={...values,startsAt:dayjs(values.dates[0]).format("YYYY-MM-DD"),endsAt:dayjs(values.dates[1]).format("YYYY-MM-DD"),...(isManager &&{status:TimeOffStatus.APPROVED,}),};awaitonFinish(payload);};
useForm מאתחל את הטופס עם ערכים ברירת מחדל ומגדיר התראות הצלחה בהתאם לתפקיד המשתמש. פונקציית onFinishHandler מעבדת את נתוני הטופס לפני שליחתם. עבור מנהלים, המצב מוגדר ל-APPROVED מיד, בעוד שבקשות של עובדים מוגשות לסקירה.
בעיצוב שלנו, הצבע הראשי משתנה בהתאם לתפקיד המשתמש. אנחנו משתמשים ב-<ThemeProvider /> כדי להחיל את הערכה הנכונה בהתאם. טקסט וסמל הכפתור לשליחה גם משתנים בהתאם להאם המשתמש הוא מנהל או עובד.
4. הוספת הנתיב "/employee/time-offs/create"
אנחנו צריכים להוסיף את הנתיב החדש עבור יצירת דף הפניות לזמן פנוי. נעדכן את הקובץ App.tsx כדי לכלול את הנתיב הזה:
לאחר הוספת השינויים האלה, ניתן לנווט לנתיב /employee/time-offs/create או ללחוץ על כפתור "הקצאת זמן פנוי" בדף רשימת הפניות לזמן פנוי כדי לגשת לטופס יצירת הפנייה לזמן פנוי.
הרכיב <RequestsList /> מציג רשימה של בקשות חופשה עם גלילה אינסופית. הוא כולל אינדיקטור טעינה, מקומות מחזיקים, והודעה כאשר אין נתונים. רכיב זה מיועד לטפל בסטים גדולים של נתונים בצורה יעילה ולספק חווית משתמש חלקה.
בניית רכיב <RequestsListItem />
צור קובץ חדש בשם list-item.tsx בתיקייה src/components/requests/ והוסף את הקוד הבא:
הרכיב <RequestsListItem /> מציג בפריט אחד ברשימת בקשות חופשה. הוא כולל אווטר של העובד, שם, תיאור וכפתור לצפייה בפרטי הבקשה. רכיב זה ניתן לשימוש חוזר וניתן להשתמש בו כדי לעדכן כל פריט ברשימת הבקשות לחופשה.
בניית רכיב <PageManagerRequestsList />
צור קובץ חדש בשם list.tsx בתיקייה src/pages/manager/requests/ והוסף את הקוד הבא:
import type {PropsWithChildren}from"react";import{ useGo, useInfiniteList }from"@refinedev/core";import{Box,Typography}from"@mui/material";importdayjsfrom"dayjs";import{Frame}from"@/components/frame";import{PageHeader}from"@/components/layout/page-header";import{RequestsListItem}from"@/components/requests/list-item";import{RequestsList}from"@/components/requests/list";import{ indigo }from"@/providers/theme-provider/colors";import{TimeOffIcon,RequestTypeIcon,NoTimeOffIcon}from"@/icons";import{TimeOffStatus, type Employee, type TimeOff}from"@/types";exportconstPageManagerRequestsList=({ children }:PropsWithChildren)=>{return(<><Box><PageHeadertitle="Awaiting Requests"/><TimeOffsList/></Box>{children}</>);};constTimeOffsList=()=>{const go =useGo();const{data: timeOffsData,isLoading: timeOffsLoading,fetchNextPage: timeOffsFetchNextPage,hasNextPage: timeOffsHasNextPage,}= useInfiniteList<TimeOff&{employee:Employee;}>({resource:"time-offs",filters:[{field:"status",operator:"eq",value:TimeOffStatus.PENDING},],sorters:[{field:"createdAt",order:"desc"}],meta:{join:["employee"],},});const timeOffs = timeOffsData?.pages.flatMap((page)=> page.data)||[];const totalCount = timeOffsData?.pages[0].total;return(<Frametitle="Time off Requests"titleSuffix={!!totalCount &&
totalCount >0&&(<Boxsx={{padding:"4px",display:"flex",alignItems:"center",justifyContent:"center",minWidth:"24px",height:"24px",borderRadius:"4px",backgroundColor: indigo[100],}}><Typographyvariant="caption"sx={{color: indigo[500],fontSize:"12px",lineHeight:"16px",}}>{totalCount}</Typography></Box>)}icon={<TimeOffIconwidth={24}height={24}/>}sx={{flex:1,paddingBottom:"0px",}}sxChildren={{padding:0,}}><RequestsListloading={timeOffsLoading}dataLength={timeOffs.length}hasMore={timeOffsHasNextPage ||false}next={timeOffsFetchNextPage}scrollableTarget="scrollableDiv-timeOffs"noDataText="No time off requests right now."noDataIcon={<NoTimeOffIcon/>}>{timeOffs.map((timeOff)=>{const date =dayjs(timeOff.createdAt).fromNow();const fullName =`${timeOff.employee.firstName}${timeOff.employee.lastName}`;const avatarURL = timeOff.employee.avatarUrl;const requestedDay =dayjs(timeOff.endsAt).diff(dayjs(timeOff.startsAt),"day")+1;const description =`Requested ${requestedDay}${
requestedDay >1?"days":"day"} of time ${timeOff.timeOffType.toLowerCase()} leave.`;return(<RequestsListItem
key={timeOff.id}
date={date}
avatarURL={avatarURL}
title={fullName}
showTimeSince
descriptionIcon={<RequestTypeIcontype={timeOff.timeOffType}/>}
description={description}
onClick={()=>{go({type:"replace",to:{resource:"requests",id: timeOff.id,action:"edit",},});}}/>);})}</RequestsList></Frame>);};
הרכיב <PageManagerRequestsList /> מציג בקשות לחופשה ממתינות שצריך לאשר מנהלים. הוא מציג פרטים כמו שם העובד, סוג החופשה, תאריכי הבקשה, וכמה זמן עבר מאז שהבקשה נעשתה. מנהלים יכולים ללחוץ על בקשה כדי לראות פרטים נוספים. הרכיב משתמש ב־<RequestsList /> ו־<RequestsListItem /> כדי לעדכן את הרשימה.
רכיב זה גם מקבל את children כפרופ. בשלב הבא, נממש נתיב מודאלי באמצעות <Outlet /> כדי להציג פרטי בקשה, ולעדכן את הנתיב /manager/requests/:id בתוך הרכיב.
הוספת נתיב "/manager/requests"
אנו צריכים להוסיף את הנתיב החדש לדף ניהול בקשות חופשה. נעשה עדכון בקובץ App.tsx כדי לכלול את הנתיב הזה:
בשלב זה, ניצור דף חדש כדי להציג את פרטי בקשת החופשה. דף זה יציג את שם העובד, את סוג החופשה, את התאריכים המבוקשים ואת הסטטוס הנוכחי. מנהלים יכולים לאשר או לדחות את הבקשה מדף זה.
בניית רכיב <TimeOffRequestModal />
ראשית, צור קובץ בשם use-get-employee-time-off-usage בתיקיית src/hooks/ והוסף את הקוד הבא:
הuseGetEmployeeTimeOffUsage משמש כדי להביא את השימוש של העובד בימי חופשה. ה-h הזה מחשב את ימי החופשה השנתיים שנותרו ואת ימי המחלה והחופשה הרגילה שכבר נעשה שימוש בהם בהתבסס על היסטוריית החופשה של העובד.
הuseList עם המסננים הנ"ל מביא את כל החופשות המאושרות החופפות עם בקשת החופשה הנוכחית. רשימה זו משמשת להציג את העובדים שנמצאים בחופשה בין התאריכים המבוקשים.
3. טיפול באישור/דחיית בקשת חופשה
הhandleSubmit נקראת כאשר המנהל מאשר או דוחה את בקשת החופשה.
Refine מבטל אוטומטית את מטמון המשאבים לאחר שמשאב שונה (time-offs במקרה זה).
מכיוון ששימוש העובד בחופשה מחושב בהתבסס על היסטוריית החופשה, אנו גם מבטלים את מטמון המשאבים של employees כדי לעדכן את השימוש של העובד בחופשה.
הוספת המסלול “/manager/requests/:id”
בשלב זה, ניצור מסלול חדש כדי להציג את פרטי בקשת החופשה, היכן שהמנהלים יכולים לאשר או לדחות בקשות.
בואו ניצור קובץ חדש בשם edit.tsx בתיקיית src/pages/manager/requests/time-offs/ ונוסיף את הקוד הבא:
src/pages/manager/requests/time-offs/edit.tsx
import{ useGo, useShow }from"@refinedev/core";import{TimeOffRequestModal}from"@/components/requests/time-off-request-modal";import type {Employee,TimeOff}from"@/types";exportconstPageManagerRequestsTimeOffsEdit=()=>{const go =useGo();const{query: timeOffRequestQuery }= useShow<TimeOff&{employee:Employee}>({meta:{join:["employee"],},});const loading = timeOffRequestQuery.isLoading;return(<TimeOffRequestModal
open
loading={loading}
timeOff={timeOffRequestQuery?.data?.data}
onClose={()=>go({to:{resource:"requests",action:"list",},})}
onSuccess={()=>{go({to:{resource:"requests",action:"list",},});}}/>);};
עכשיו אנחנו צריכים להוסיף את המסלול החדש כדי להציג את פרטי בקשת החופשה. בואו נעדכן את הקובץ App.tsx כדי לכלול את המסלול הזה:
הקוד למעלה מגדיר מבנה מסלולים מקונן שבו מוצג מודל כאשר נווטים למסלול ילד ספציפי. רכיב ה-<PageManagerRequestsTimeOffsEdit /> הוא מודל ומוצג כילד של רכיב <PageManagerRequestsList />. מבנה זה מאפשר לנו להציג את המודל מעל לדף הרשימה תוך שמירה על דף הרשימה גלוי ברקע.
כאשר אתה נווט למסלול /manager/requests/:id/edit או לוחץ על בקשת חופשה ברשימה, דף פרטי בקשת החופשה יוצג כמוקפץ מעל לדף הרשימה.
האישור הוא רכיב חיוני ביישומים ברמת העסקים, תורם לשיפור האבטחה וליעילות הפעולתית. הוא מבטיח כי רק משתמשים מורשים יכולים לגשת למשאבים מסוימים, מגן על נתונים רגישים ופונקציות. מערכת האישור של Refine מספקת את התשתית הנחוצה כדי להגן על המשאבים שלך ולוודא כי המשתמשים מתקשרים עם היישום שלך באופן מאובטח ובמקום בצורה שליטה. בשלב זה, נממש אישור ובקרת גישה עבור יכולת ניהול בקשות לחופשות. נמנע גישה לנתיבים /manager/requests ו־/manager/requests/:id/edit למנהלים בלבד בעזרת הרכיב <CanAccess />.
כרגע, כאשר אתה מתחבר כעובד, אינך רואה את קישור דף Requests בסרגל הצד אך עדיין יכול לגשת לנתיב /manager/requests על ידי הקלדת ה-URL בדפדפן. נוסיף שומר כדי למנוע גישה לא מורשית לנתיבים אלה.
בואו נעדכן את קובץ ה־App.tsx כדי לכלול בדיקות אישור:
בקוד לעיל, הוספנו את הרכיב <CanAccess /> לנתיב "/manager". רכיב זה בודק אם למשתמש יש תפקיד "מנהל" לפני עיבוד נתיבי הילד. אם אין למשתמש את התפקיד "מנהל", הוא יופנה לדף רשימת החופשות עבור עובדים.
כעת, כאשר אתה מתחבר כעובד ומנסה לגשת לנתיב /manager/requests, תועבר לדף רשימת חופשות עבור עובדים.
לאחר מכן, ציין כי ברצונך לדחוף את הקוד שלך לענף main בעזרת הפקודה הזו:
git branch -M main
סוף סוף, הזן את הקוד למאגר הקוד של GitHub עם הפקודה הבאה:
git push -u origin main
כאשר יתבקש, הזן את הפרטים שלך ב-GitHub כדי להעלות את הקוד שלך.
תקבל הודעת הצלחה לאחר שהקוד מועלה למאגר הקוד של GitHub.
בחלק זה, העלית את הפרוייקט שלך ל-GitHub כדי שתוכל לגשת אליו באמצעות DigitalOcean Apps. השלב הבא הוא ליצור אפליקציית DigitalOcean חדשה באמצעות הפרוייקט שלך ולהגדיר פרסום אוטומטי.
במהלך זה, תכין אפליקציית React ותכין אותה לפרסום דרך פלטפורמת App של DigitalOcean. תקשר את מאגר הקוד שלך ב-GitHub ל-DigitalOcean, תגדיר כיצד האפליקציה תבנה, ותיצור פרסום ראשוני של הפרוייקט. לאחר שהפרוייקט מופץ, שינויים נוספים שתבצע יושפעו באופן אוטומטי ויעודכנו.
עד סוף שלב זה, יהיה לך את האפליקציה שלך מופצת ב-DigitalOcean עם מסירה רציפה נלקחת בחשבון.
התחבר לחשבון ה-DigitalOcean שלך ונווט לעמוד Apps. לחץ על הכפתור Create App:
אם עדיין לא חיברת את חשבון GitHub שלך ל-DigitalOcean, תתבקש לעשות זאת. לחץ על כפתור Connect to GitHub. חלון חדש ייפתח, שבו תתבקש לאשר ל-DigitalOcean לגשת לחשבון GitHub שלך.
לאחר שתאשר את DigitalOcean, אתה תועבר חזרה לעמוד האפליקציות של DigitalOcean. השלב הבא הוא לבחור את מאגר ה-GitHub שלך. לאחר שתבחר את המאגר שלך, תתבקש לבחור ענף לפריסה. בחר את ענף main ולחץ על כפתור Next.
לאחר מכן, תראה את שלבי ההגדרה עבור האפליקציה שלך. במדריך זה, תוכל ללחוץ על כפתור Next כדי לדלג על שלבי ההגדרה. עם זאת, תוכל גם להגדיר את האפליקציה שלך כפי שתרצה.
חכה שהבנייה תושלם. לאחר שהבנייה הושלמה, לחץ על Live App כדי לגשת לפרויקט שלך בדפדפן. זה יהיה אותו פרויקט שבדקת מקומית, אבל זה יהיה חי באינטרנט עם כתובת URL מאובטחת. כמו כן, תוכל לעקוב אחרי המדריך הזה הזמין באתר הקהילה של DigitalOcean כדי ללמוד כיצד לפרוס אפליקציות מבוססות React ל-App Platform.
הערה: במקרה שהבנייה שלך לא מצליחה להיפרס בהצלחה, תוכל להגדיר את פקודת הבנייה שלך ב-DigitalOcean להשתמש ב-npm install --production=false && npm run build && npm prune --production במקום npm run build