Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
T
thecybernanny-webapp
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
asranov0003
thecybernanny-webapp
Commits
d33f7701
Commit
d33f7701
authored
Aug 28, 2025
by
asranov0003
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: post time limits
parent
b3440087
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
251 additions
and
19 deletions
+251
-19
en.json
src/locales/en/en.json
+31
-4
ru.json
src/locales/ru/ru.json
+31
-4
uz.json
src/locales/uz/uz.json
+31
-4
UsageLimits.tsx
src/pages/UsageLimits/UsageLimits.tsx
+119
-7
usageLimitSlice.ts
src/stores/slices/usageLimitSlice.ts
+39
-0
No files found.
src/locales/en/en.json
View file @
d33f7701
...
...
@@ -16,7 +16,11 @@
"selectDevice"
:
"Select Device"
,
"noData"
:
"No data found"
,
"downloadForAndroid"
:
"Download for Android"
,
"downloadForIos"
:
"Download for iOS"
"downloadForIos"
:
"Download for iOS"
,
"pageInDevelopment"
:
"Page in Development"
,
"pageInDevelopmentDesc"
:
"This page is currently under development. Use the application to view recordings."
,
"hours"
:
"Hours"
,
"minutes"
:
"Minutes"
},
"auth"
:
{
"entrance"
:
"Sign In"
,
...
...
@@ -29,6 +33,7 @@
"forgetPassword"
:
"Forgot password?"
,
"recover"
:
"Recover"
,
"passwordRecovery"
:
"Password recovery"
,
"passwordChange"
:
"Password change"
,
"newPassword"
:
"New password"
,
"newPasswordPlaceholder"
:
"Enter new password"
,
"repeatPassword"
:
"Repeat password"
,
...
...
@@ -107,6 +112,8 @@
"account"
:
"Account"
,
"deleteAccount"
:
"Delete account"
,
"recoverPassword"
:
"Recover password"
,
"changePassword"
:
"Change password"
,
"subscribeNow"
:
"Subscribe now"
,
"logout"
:
"Logout"
},
"permissions"
:
{
...
...
@@ -144,12 +151,23 @@
},
"messengers"
:
{
"title"
:
"Chats"
,
"empty"
:
"No chats found."
"messages"
:
"Messages"
,
"allChats"
:
"All chats"
,
"empty"
:
"No chats found."
,
"from"
:
"From"
,
"to"
:
"To"
},
"usageLimits"
:
{
"title"
:
"Phone usage time limits"
,
"desc"
:
"Set a daily phone usage limit."
,
"allowedApps"
:
"Allowed apps will be still accessible."
"allowedApps"
:
"Allowed apps will be still accessible."
,
"setLimit"
:
"Set a limit"
,
"hoursRequired"
:
"Hours are required"
,
"minHour"
:
"Minimum hours is 0"
,
"maxHour"
:
"Maximum hours is 23"
,
"minutesRequired"
:
"Minutes are required"
,
"minMinutes"
:
"Minimum minutes is 0"
,
"maxMinutes"
:
"Maximum minutes is 59"
},
"pincode"
:
{
"enterTitle"
:
"Enter your PIN Code"
,
...
...
@@ -180,7 +198,16 @@
"selectTariff"
:
"Select a tariff"
,
"selectPaymentMethod"
:
"Select a payment method"
,
"cancellPaymentTitle"
:
"Payment can be cancelled within 24 hours"
,
"pay"
:
"Pay for a subscription"
"pay"
:
"Pay for a subscription"
,
"notActive"
:
"Subscription is not active"
,
"notActiveDesc"
:
"The functionality of the cabinet has been limited. To restore functionality, you need to subscribe."
,
"toTariffs"
:
"To tariffs"
},
"download"
:
{
"android"
:
"Download for Android"
,
"ios"
:
"Download for iOS"
,
"rustore"
:
"Download from RuStore"
,
"webApp"
:
"TgApp - for parents' convenience"
},
"button"
:
{
"login"
:
"Login"
,
...
...
src/locales/ru/ru.json
View file @
d33f7701
...
...
@@ -16,7 +16,11 @@
"selectDevice"
:
"Выбрать устройство"
,
"noData"
:
"Данные не найдены"
,
"downloadForAndroid"
:
"Скачать для Android"
,
"downloadForIos"
:
"Скачать для iOS"
"downloadForIos"
:
"Скачать для iOS"
,
"pageInDevelopment"
:
"Страница в разработке"
,
"pageInDevelopmentDesc"
:
"Эта страница в настоящее время разрабатывается. Используйте приложение для просмотра записей."
,
"hours"
:
"Часы"
,
"minutes"
:
"Минуты"
},
"auth"
:
{
"entrance"
:
"Вход"
,
...
...
@@ -29,6 +33,7 @@
"forgetPassword"
:
"Забыли пароль?"
,
"recover"
:
"Восстановить"
,
"passwordRecovery"
:
"Восстановление пароля"
,
"passwordChange"
:
"Смена пароля"
,
"newPassword"
:
"Новый пароль"
,
"newPasswordPlaceholder"
:
"Введите новый пароль"
,
"repeatPassword"
:
"Повторите пароль"
,
...
...
@@ -107,6 +112,8 @@
"account"
:
"Аккаунт"
,
"deleteAccount"
:
"Удалить аккаунт"
,
"recoverPassword"
:
"Восстановить пароль"
,
"changePassword"
:
"Сменить пароль"
,
"subscribeNow"
:
"Оформить подписку"
,
"logout"
:
"Выйти"
},
"permissions"
:
{
...
...
@@ -144,12 +151,23 @@
},
"messengers"
:
{
"title"
:
"Чаты"
,
"empty"
:
"Чаты не найдены."
"messages"
:
"Сообщения"
,
"allChats"
:
"Все чаты"
,
"empty"
:
"Чаты не найдены."
,
"from"
:
"От"
,
"to"
:
"Кому"
},
"usageLimits"
:
{
"title"
:
"Ограничения времени использования телефона"
,
"desc"
:
"Установите дневное ограничение на использование телефона."
,
"allowedApps"
:
"Разрешенные приложения будут по-прежнему доступны."
"allowedApps"
:
"Разрешенные приложения будут по-прежнему доступны."
,
"setLimit"
:
"Установить лимит"
,
"hoursRequired"
:
"Часы обязательны"
,
"minHour"
:
"Минимум часов - 0"
,
"maxHour"
:
"Максимум часов - 23"
,
"minutesRequired"
:
"Минуты обязательны"
,
"minMinutes"
:
"Минимум минут - 0"
,
"maxMinutes"
:
"Максимум минут - 59"
},
"pincode"
:
{
"enterTitle"
:
"Введите PIN-код"
,
...
...
@@ -180,7 +198,16 @@
"selectTariff"
:
"Выберите тариф"
,
"selectPaymentMethod"
:
"Выберите способ оплаты"
,
"cancellPaymentTitle"
:
"Платеж можно отменить в течении 24 часов"
,
"pay"
:
"Оплатить подписку"
"pay"
:
"Оплатить подписку"
,
"notActive"
:
"Подписка не активна"
,
"notActiveDesc"
:
"Функционал кабинета был ограничен. Для восстановления функционала необходимо оформить подписку."
,
"toTariffs"
:
"К тарифам"
},
"download"
:
{
"android"
:
"Скачать для Android"
,
"ios"
:
"Скачать для iOS"
,
"rustore"
:
"Скачать из RuStore"
,
"webApp"
:
"TgApp - для удобства родителей"
},
"button"
:
{
"login"
:
"Войти"
,
...
...
src/locales/uz/uz.json
View file @
d33f7701
...
...
@@ -16,7 +16,11 @@
"selectDevice"
:
"Qurilmani tanlang"
,
"noData"
:
"Hech qanday ma'lumot topilmadi"
,
"downloadForAndroid"
:
"Android uchun yuklab olish"
,
"downloadForIos"
:
"iOS uchun yuklab olish"
"downloadForIos"
:
"iOS uchun yuklab olish"
,
"pageInDevelopment"
:
"Sahifa ishlab chiqilmoqda"
,
"pageInDevelopmentDesc"
:
"Ushbu sahifa hozirda ishlab chiqilmoqda. Yozuvlarni ko'rish uchun ilovadan foydalaning."
,
"hours"
:
"Soatlar"
,
"minutes"
:
"Daqiqalar"
},
"auth"
:
{
"entrance"
:
"Kirish"
,
...
...
@@ -29,6 +33,7 @@
"forgetPassword"
:
"Parolni unutdingizmi?"
,
"recover"
:
"Tiklash"
,
"passwordRecovery"
:
"Parolni tiklash"
,
"passwordChange"
:
"Parolni o'zgartirish"
,
"newPassword"
:
"Yangi parol"
,
"newPasswordPlaceholder"
:
"Yangi parolni kiriting"
,
"repeatPassword"
:
"Parolni takrorlang"
,
...
...
@@ -107,6 +112,8 @@
"account"
:
"Hisob"
,
"deleteAccount"
:
"Hisobni o'chirish"
,
"recoverPassword"
:
"Parolni tiklash"
,
"changePassword"
:
"Parolni o'zgartirish"
,
"subscribeNow"
:
"Obuna bo'lish"
,
"logout"
:
"Chiqish"
},
"permissions"
:
{
...
...
@@ -144,12 +151,23 @@
},
"messengers"
:
{
"title"
:
"Chats"
,
"empty"
:
"Xabarlar topilmadi."
"messages"
:
"Xabarlar"
,
"allChats"
:
"Barcha chatlar"
,
"empty"
:
"Chatlar topilmadi."
,
"from"
:
"Kimdan"
,
"to"
:
"Kimga"
},
"usageLimits"
:
{
"title"
:
"Telefon foydalanish vaqti chegaralari"
,
"desc"
:
"Kunlik telefon foydalanish cheklovini o'rnating."
,
"allowedApps"
:
"Ruxsat berilgan ilovalar hali ham foydalanishga ochiq bo'ladi."
"allowedApps"
:
"Ruxsat berilgan ilovalar hali ham foydalanishga ochiq bo'ladi."
,
"setLimit"
:
"Cheklov o'rnating"
,
"hoursRequired"
:
"Soatlar talab qilinadi"
,
"minHour"
:
"Minimal soatlar - 0"
,
"maxHour"
:
"Maksimal soatlar - 23"
,
"minutesRequired"
:
"Daqiqalar talab qilinadi"
,
"minMinutes"
:
"Minimal daqiqalar - 0"
,
"maxMinutes"
:
"Maksimal daqiqalar - 59"
},
"pincode"
:
{
"enterTitle"
:
"PIN kodni kiriting"
,
...
...
@@ -180,7 +198,16 @@
"selectTariff"
:
"Tarifni tanlang"
,
"selectPaymentMethod"
:
"To'lov usulini tanlang"
,
"cancellPaymentTitle"
:
"To'lovni 24 soat ichida bekor qilish mumkin"
,
"pay"
:
"Obunani to'lash"
"pay"
:
"Obunani to'lash"
,
"notActive"
:
"Obuna faol emas"
,
"notActiveDesc"
:
"Shaxsiy kabinet funksiyalari cheklangan. Funktsiyalarni tiklash uchun obuna bo'lishingiz kerak."
,
"toTariffs"
:
"Tariflarga"
},
"download"
:
{
"android"
:
"Android uchun yuklab olish"
,
"ios"
:
"iOS uchun yuklab olish"
,
"rustore"
:
"RuStore dan yuklab olish"
,
"webApp"
:
"TgApp - ota-onalar uchun qulaylik"
},
"button"
:
{
"login"
:
"Kirish"
,
...
...
src/pages/UsageLimits/UsageLimits.tsx
View file @
d33f7701
import
React
,
{
useEffect
}
from
"react"
;
import
React
,
{
useEffect
,
useState
}
from
"react"
;
import
"./UsageLimits.css"
;
import
SectionHeader
from
"../../layouts/SectionHeader"
;
import
CButton
from
"../../components/CButton"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
{
useAppDispatch
,
type
RootState
}
from
"../../stores/store"
;
import
{
useSelector
}
from
"react-redux"
;
import
{
fetchTimeLimits
}
from
"../../stores/slices/usageLimitSlice"
;
import
{
fetchTimeLimits
,
postTimeLimits
,
}
from
"../../stores/slices/usageLimitSlice"
;
import
CLoading
from
"../../components/CLoading"
;
import
{
useForm
}
from
"react-hook-form"
;
import
CInput
from
"../../components/CInput"
;
import
CModal
from
"../../components/CModal"
;
type
FormValues
=
{
hours
:
string
;
minutes
:
string
;
};
const
UsageLimits
:
React
.
FC
=
()
=>
{
const
[
isOpenModal
,
setIsOpenModal
]
=
useState
(
false
);
const
[
selectedDayIndex
,
setSelectedDayIndex
]
=
useState
<
number
|
null
>
(
null
);
const
{
selectedDevice
}
=
useSelector
((
state
:
RootState
)
=>
state
.
device
);
const
{
days
,
isLoadingDays
}
=
useSelector
(
const
{
days
,
isLoadingDays
,
loadingPostLimits
}
=
useSelector
(
(
state
:
RootState
)
=>
state
.
usageLimit
);
const
dispatch
=
useAppDispatch
();
const
{
t
}
=
useTranslation
();
const
{
register
,
setValue
,
handleSubmit
,
formState
:
{
errors
},
}
=
useForm
<
FormValues
>
({
defaultValues
:
{
hours
:
""
,
minutes
:
""
,
},
});
const
onToggleModal
=
()
=>
{
setIsOpenModal
((
prev
)
=>
!
prev
);
};
const
openDayModal
=
(
day
:
number
,
index
:
number
)
=>
{
setSelectedDayIndex
(
index
);
const
hrs
=
Math
.
floor
(
day
/
60
);
const
mins
=
day
%
60
;
setValue
(
"hours"
,
String
(
hrs
));
setValue
(
"minutes"
,
String
(
mins
));
setIsOpenModal
(
true
);
};
const
onSubmit
=
async
(
data
:
FormValues
)
=>
{
if
(
selectedDayIndex
===
null
||
!
selectedDevice
?.
id
)
return
;
const
hrs
=
parseInt
(
data
.
hours
)
||
0
;
const
mins
=
parseInt
(
data
.
minutes
)
||
0
;
const
totalMinutes
=
hrs
*
60
+
mins
;
const
updatedDays
=
[...
days
];
updatedDays
[
selectedDayIndex
]
=
totalMinutes
;
try
{
await
dispatch
(
postTimeLimits
({
deviceId
:
selectedDevice
.
id
,
days
:
updatedDays
,
})
).
unwrap
();
setIsOpenModal
(
false
);
}
catch
(
error
)
{
console
.
error
(
"Error updating time limits: "
,
error
);
}
};
useEffect
(()
=>
{
if
(
!
selectedDevice
?.
id
)
return
;
dispatch
(
fetchTimeLimits
(
selectedDevice
?.
id
));
},
[
dispatch
,
selectedDevice
]);
...
...
@@ -58,7 +123,11 @@ const UsageLimits: React.FC = () => {
<
div
className=
"usagelimits__days"
>
{
days
.
map
((
day
,
index
)
=>
{
return
(
<
div
className=
"usagelimits__day"
key=
{
index
}
>
<
div
className=
"usagelimits__day"
key=
{
index
}
onClick=
{
()
=>
openDayModal
(
day
,
index
)
}
>
<
p
>
{
weekDays
[
index
]
}
</
p
>
<
p
>
{
formatTime
(
day
)
}
</
p
>
</
div
>
...
...
@@ -67,9 +136,52 @@ const UsageLimits: React.FC = () => {
</
div
>
)
}
</
div
>
<
CButton
title=
{
t
(
"button.save"
)
}
/>
</
div
>
<
CModal
isOpen=
{
isOpenModal
}
onToggle=
{
onToggleModal
}
content=
{
<
form
className=
"modal__box"
onSubmit=
{
handleSubmit
(
onSubmit
)
}
>
<
h3
className=
"modal__box__title"
>
Set a limit
</
h3
>
<
div
className=
"modal__box__actions"
>
<
CInput
label=
"Hours"
placeholder=
"0"
{
...
register
("
hours
",
{
required
:
"
Hours
are
required
",
min
:
{
value
:
0,
message
:
"
Minimum
hours
is
0"
},
max
:
{
value
:
23,
message
:
"
Maximum
hours
is
23"
},
})}
error=
{
errors
.
hours
?.
message
}
/>
<
CInput
label=
"Minutes"
placeholder=
"0"
{
...
register
("
minutes
",
{
required
:
"
Minutes
are
required
",
min
:
{
value
:
0,
message
:
"
Minimum
minutes
is
0"
},
max
:
{
value
:
59,
message
:
"
Maximum
minutes
is
59"
},
})}
error=
{
errors
.
minutes
?.
message
}
/>
</
div
>
<
div
className=
"modal__box__actions"
>
<
CButton
title=
{
t
(
"button.cancel"
)
}
onClick=
{
onToggleModal
}
/>
<
CButton
title=
{
t
(
"button.confirm"
)
}
type=
"submit"
variant=
"primary"
isLoading=
{
loadingPostLimits
}
/>
</
div
>
</
form
>
}
/>
</
div
>
);
};
...
...
src/stores/slices/usageLimitSlice.ts
View file @
d33f7701
...
...
@@ -4,12 +4,14 @@ import { sendRpcRequest } from "../../services/apiClient";
interface
IUsageLimitState
{
days
:
number
[];
isLoadingDays
:
boolean
;
loadingPostLimits
:
boolean
;
errorDays
:
string
|
null
;
}
const
initialState
:
IUsageLimitState
=
{
days
:
[],
isLoadingDays
:
false
,
loadingPostLimits
:
false
,
errorDays
:
null
,
};
...
...
@@ -35,6 +37,31 @@ export const fetchTimeLimits = createAsyncThunk(
}
);
export
const
postTimeLimits
=
createAsyncThunk
(
"timeLimits/postTimeLimits"
,
async
(
{
deviceId
,
days
}:
{
deviceId
:
string
;
days
:
number
[]
},
{
rejectWithValue
}
)
=>
{
try
{
const
response
=
await
sendRpcRequest
<
{
days
:
number
[]
}
>
(
"apps.timelimitset"
,
{
deviceId
,
days
,
}
);
return
response
.
days
;
}
catch
(
error
:
unknown
)
{
if
(
typeof
error
===
"object"
&&
error
!==
null
&&
"message"
in
error
)
{
return
rejectWithValue
(
error
.
message
);
}
return
rejectWithValue
(
"An unknown error occurred"
);
}
}
);
const
usageLimitSlice
=
createSlice
({
name
:
"usageLimit"
,
initialState
,
...
...
@@ -52,6 +79,18 @@ const usageLimitSlice = createSlice({
.
addCase
(
fetchTimeLimits
.
rejected
,
(
state
,
action
)
=>
{
state
.
isLoadingDays
=
false
;
state
.
errorDays
=
action
.
payload
as
string
;
})
.
addCase
(
postTimeLimits
.
pending
,
(
state
)
=>
{
state
.
loadingPostLimits
=
true
;
})
.
addCase
(
postTimeLimits
.
fulfilled
,
(
state
,
action
)
=>
{
state
.
loadingPostLimits
=
false
;
state
.
days
=
action
.
payload
;
})
.
addCase
(
postTimeLimits
.
rejected
,
(
state
,
action
)
=>
{
state
.
loadingPostLimits
=
false
;
state
.
errorDays
=
action
.
payload
as
string
;
});
},
});
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment