mirror of https://github.com/Qortal/q-tube
Browse Source
Refactored constants/index.ts into Identifiers.ts, Categories.ts, and Misc.ts. Regular expressions that titles allow all use new variable in Misc.ts for consistency and ease of editing it. New Characters are allowed in titles. Categories sorted by name, "Other" is always at end of list. New Categories such as Qortal under Education have been added Title prefix TextField added that starts all video titles with the entered value.pull/1/head
Qortal Dev
9 months ago
31 changed files with 2417 additions and 1867 deletions
@ -0,0 +1,10 @@ |
|||||||
|
{ |
||||||
|
"printWidth": 80, |
||||||
|
"singleQuote": false, |
||||||
|
"trailingComma": "es5", |
||||||
|
"bracketSpacing": true, |
||||||
|
"jsxBracketSameLine": false, |
||||||
|
"arrowParens": "avoid", |
||||||
|
"tabWidth": 2, |
||||||
|
"semi": true |
||||||
|
} |
@ -1,66 +1,83 @@ |
|||||||
import React from 'react' |
import React from "react"; |
||||||
import { CardContentContainerComment } from '../common/Comments/Comments-styles' |
import { CardContentContainerComment } from "../common/Comments/Comments-styles"; |
||||||
import { CrowdfundSubTitle, CrowdfundSubTitleRow } from '../UploadVideo/Upload-styles' |
import { |
||||||
import { Box, Typography, useTheme } from '@mui/material' |
CrowdfundSubTitle, |
||||||
import { useNavigate } from 'react-router-dom' |
CrowdfundSubTitleRow, |
||||||
|
} from "../PublishVideo/PublishVideo-styles.tsx"; |
||||||
export const Playlists = ({playlistData, currentVideoIdentifier, onClick}) => { |
import { Box, Typography, useTheme } from "@mui/material"; |
||||||
const theme = useTheme(); |
import { useNavigate } from "react-router-dom"; |
||||||
const navigate = useNavigate() |
|
||||||
|
|
||||||
|
export const Playlists = ({ |
||||||
|
playlistData, |
||||||
|
currentVideoIdentifier, |
||||||
|
onClick, |
||||||
|
}) => { |
||||||
|
const theme = useTheme(); |
||||||
|
const navigate = useNavigate(); |
||||||
|
|
||||||
return ( |
return ( |
||||||
<Box sx={{ |
<Box |
||||||
display: 'flex', |
sx={{ |
||||||
flexDirection: 'column', |
display: "flex", |
||||||
|
flexDirection: "column", |
||||||
|
|
||||||
maxWidth: '400px', |
maxWidth: "400px", |
||||||
width: '100%' |
width: "100%", |
||||||
}}> |
}} |
||||||
<CrowdfundSubTitleRow > |
> |
||||||
|
<CrowdfundSubTitleRow> |
||||||
<CrowdfundSubTitle>Playlist</CrowdfundSubTitle> |
<CrowdfundSubTitle>Playlist</CrowdfundSubTitle> |
||||||
</CrowdfundSubTitleRow> |
</CrowdfundSubTitleRow> |
||||||
<CardContentContainerComment sx={{ |
<CardContentContainerComment |
||||||
marginTop: '25px', |
sx={{ |
||||||
height: '450px', |
marginTop: "25px", |
||||||
overflow: 'auto' |
height: "450px", |
||||||
}}> |
overflow: "auto", |
||||||
{playlistData?.videos?.map((vid, index)=> { |
}} |
||||||
const isCurrentVidPlayling = vid?.identifier === currentVideoIdentifier; |
> |
||||||
|
{playlistData?.videos?.map((vid, index) => { |
||||||
|
const isCurrentVidPlayling = |
||||||
|
vid?.identifier === currentVideoIdentifier; |
||||||
|
|
||||||
return ( |
return ( |
||||||
<Box key={vid?.identifier} sx={{ |
<Box |
||||||
display: 'flex', |
key={vid?.identifier} |
||||||
gap: '10px', |
sx={{ |
||||||
width: '100%', |
display: "flex", |
||||||
background: isCurrentVidPlayling && theme.palette.primary.main, |
gap: "10px", |
||||||
alignItems: 'center', |
width: "100%", |
||||||
padding: '10px', |
background: isCurrentVidPlayling && theme.palette.primary.main, |
||||||
borderRadius: '5px', |
alignItems: "center", |
||||||
cursor: isCurrentVidPlayling ? 'default' : 'pointer', |
padding: "10px", |
||||||
userSelect: 'none' |
borderRadius: "5px", |
||||||
|
cursor: isCurrentVidPlayling ? "default" : "pointer", |
||||||
|
userSelect: "none", |
||||||
|
}} |
||||||
|
onClick={() => { |
||||||
|
if (isCurrentVidPlayling) return; |
||||||
|
onClick(vid.name, vid.identifier); |
||||||
|
// navigate(`/video/${vid.name}/${vid.identifier}`)
|
||||||
|
}} |
||||||
|
> |
||||||
|
<Typography |
||||||
|
sx={{ |
||||||
|
fontSize: "14px", |
||||||
}} |
}} |
||||||
onClick={()=> { |
> |
||||||
if(isCurrentVidPlayling) return |
{index + 1} |
||||||
onClick(vid.name, vid.identifier) |
</Typography> |
||||||
// navigate(`/video/${vid.name}/${vid.identifier}`)
|
<Typography |
||||||
|
sx={{ |
||||||
|
fontSize: "18px", |
||||||
|
wordBreak: "break-word", |
||||||
}} |
}} |
||||||
> |
> |
||||||
<Typography sx={{ |
{vid?.metadata?.title} |
||||||
fontSize: '14px' |
</Typography> |
||||||
}}>{index + 1}</Typography> |
</Box> |
||||||
<Typography sx={{ |
); |
||||||
fontSize: '18px', |
|
||||||
wordBreak: 'break-word' |
|
||||||
}}>{vid?.metadata?.title}</Typography> |
|
||||||
|
|
||||||
</Box> |
|
||||||
) |
|
||||||
})} |
})} |
||||||
</CardContentContainerComment> |
</CardContentContainerComment> |
||||||
</Box> |
</Box> |
||||||
|
); |
||||||
) |
}; |
||||||
} |
|
||||||
|
@ -1,324 +1,347 @@ |
|||||||
import { Badge, Box, Button, List, ListItem, ListItemText, Popover, Typography } from '@mui/material' |
import { |
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' |
Badge, |
||||||
import { useDispatch, useSelector } from 'react-redux' |
Box, |
||||||
import { RootState } from '../../../state/store' |
Button, |
||||||
import { FOR, FOR_SUPER_LIKE, SUPER_LIKE_BASE, minPriceSuperlike } from '../../../constants' |
List, |
||||||
import NotificationsIcon from '@mui/icons-material/Notifications' |
ListItem, |
||||||
import { formatDate } from '../../../utils/time' |
ListItemText, |
||||||
|
Popover, |
||||||
|
Typography, |
||||||
|
} from "@mui/material"; |
||||||
|
import React, { |
||||||
|
useCallback, |
||||||
|
useEffect, |
||||||
|
useMemo, |
||||||
|
useRef, |
||||||
|
useState, |
||||||
|
} from "react"; |
||||||
|
import { useDispatch, useSelector } from "react-redux"; |
||||||
|
import { RootState } from "../../../state/store"; |
||||||
|
import NotificationsIcon from "@mui/icons-material/Notifications"; |
||||||
|
import { formatDate } from "../../../utils/time"; |
||||||
import ThumbUpIcon from "@mui/icons-material/ThumbUp"; |
import ThumbUpIcon from "@mui/icons-material/ThumbUp"; |
||||||
import { extractSigValue, getPaymentInfo, isTimestampWithinRange } from '../../../pages/VideoContent/VideoContent' |
import { |
||||||
import { useNavigate } from 'react-router-dom' |
extractSigValue, |
||||||
|
getPaymentInfo, |
||||||
|
isTimestampWithinRange, |
||||||
|
} from "../../../pages/VideoContent/VideoContent"; |
||||||
|
import { useNavigate } from "react-router-dom"; |
||||||
import localForage from "localforage"; |
import localForage from "localforage"; |
||||||
import moment from 'moment' |
import moment from "moment"; |
||||||
|
import { |
||||||
|
FOR, |
||||||
|
FOR_SUPER_LIKE, |
||||||
|
SUPER_LIKE_BASE, |
||||||
|
} from "../../../constants/Identifiers.ts"; |
||||||
|
import { minPriceSuperlike } from "../../../constants/Misc.ts"; |
||||||
|
|
||||||
const generalLocal = localForage.createInstance({ |
const generalLocal = localForage.createInstance({ |
||||||
name: "q-tube-general", |
name: "q-tube-general", |
||||||
}); |
}); |
||||||
export function extractIdValue(metadescription) { |
export function extractIdValue(metadescription) { |
||||||
// Function to extract the substring within double asterisks
|
// Function to extract the substring within double asterisks
|
||||||
function extractSubstring(str) { |
function extractSubstring(str) { |
||||||
const match = str.match(/\*\*(.*?)\*\*/); |
const match = str.match(/\*\*(.*?)\*\*/); |
||||||
return match ? match[1] : null; |
return match ? match[1] : null; |
||||||
} |
} |
||||||
|
|
||||||
// Function to extract the 'sig' value
|
// Function to extract the 'sig' value
|
||||||
function extractSig(str) { |
function extractSig(str) { |
||||||
const regex = /id:(.*?)(;|$)/; |
const regex = /id:(.*?)(;|$)/; |
||||||
const match = str.match(regex); |
const match = str.match(regex); |
||||||
return match ? match[1] : null; |
return match ? match[1] : null; |
||||||
} |
} |
||||||
|
|
||||||
// Extracting the relevant substring
|
// Extracting the relevant substring
|
||||||
const relevantSubstring = extractSubstring(metadescription); |
const relevantSubstring = extractSubstring(metadescription); |
||||||
|
|
||||||
if (relevantSubstring) { |
if (relevantSubstring) { |
||||||
// Extracting the 'sig' value
|
// Extracting the 'sig' value
|
||||||
return extractSig(relevantSubstring); |
return extractSig(relevantSubstring); |
||||||
} else { |
} else { |
||||||
return null; |
return null; |
||||||
} |
|
||||||
} |
} |
||||||
|
} |
||||||
|
|
||||||
export const Notifications = () => { |
export const Notifications = () => { |
||||||
const dispatch = useDispatch() |
const dispatch = useDispatch(); |
||||||
const [anchorElNotification, setAnchorElNotification] = useState<HTMLButtonElement | null>(null) |
const [anchorElNotification, setAnchorElNotification] = |
||||||
const [notifications, setNotifications] = useState<any[]>([]) |
useState<HTMLButtonElement | null>(null); |
||||||
const [notificationTimestamp, setNotificationTimestamp] = useState<null | number>(null) |
const [notifications, setNotifications] = useState<any[]>([]); |
||||||
|
const [notificationTimestamp, setNotificationTimestamp] = useState< |
||||||
|
null | number |
||||||
const username = useSelector((state: RootState) => state.auth?.user?.name); |
>(null); |
||||||
const usernameAddress = useSelector((state: RootState) => state.auth?.user?.address); |
|
||||||
const navigate = useNavigate(); |
const username = useSelector((state: RootState) => state.auth?.user?.name); |
||||||
|
const usernameAddress = useSelector( |
||||||
const interval = useRef<any>(null) |
(state: RootState) => state.auth?.user?.address |
||||||
|
); |
||||||
const getInitialTimestamp = async ()=> { |
const navigate = useNavigate(); |
||||||
const timestamp: undefined | number = await generalLocal.getItem("notification-timestamp"); |
|
||||||
if(timestamp){ |
const interval = useRef<any>(null); |
||||||
setNotificationTimestamp(timestamp) |
|
||||||
} |
const getInitialTimestamp = async () => { |
||||||
|
const timestamp: undefined | number = await generalLocal.getItem( |
||||||
|
"notification-timestamp" |
||||||
|
); |
||||||
|
if (timestamp) { |
||||||
|
setNotificationTimestamp(timestamp); |
||||||
} |
} |
||||||
|
}; |
||||||
useEffect(()=> { |
|
||||||
getInitialTimestamp() |
useEffect(() => { |
||||||
}, []) |
getInitialTimestamp(); |
||||||
|
}, []); |
||||||
|
|
||||||
const openNotificationPopover = (event: any) => { |
const openNotificationPopover = (event: any) => { |
||||||
const target = event.currentTarget as unknown as HTMLButtonElement | null |
const target = event.currentTarget as unknown as HTMLButtonElement | null; |
||||||
setAnchorElNotification(target) |
setAnchorElNotification(target); |
||||||
} |
}; |
||||||
const closeNotificationPopover = () => { |
const closeNotificationPopover = () => { |
||||||
setAnchorElNotification(null) |
setAnchorElNotification(null); |
||||||
} |
}; |
||||||
const fullNotifications = useMemo(() => { |
const fullNotifications = useMemo(() => { |
||||||
return [...notifications].sort( |
return [...notifications].sort((a, b) => b.created - a.created); |
||||||
(a, b) => b.created - a.created |
}, [notifications]); |
||||||
) |
const notificationBadgeLength = useMemo(() => { |
||||||
}, [notifications]) |
if (!notificationTimestamp) return fullNotifications.length; |
||||||
const notificationBadgeLength = useMemo(()=> { |
return fullNotifications?.filter( |
||||||
if(!notificationTimestamp) return fullNotifications.length |
item => item.created > notificationTimestamp |
||||||
return fullNotifications?.filter((item)=> item.created > notificationTimestamp).length |
).length; |
||||||
}, [fullNotifications, notificationTimestamp]) |
}, [fullNotifications, notificationTimestamp]); |
||||||
|
|
||||||
const checkNotifications = useCallback(async (username: string) => { |
const checkNotifications = useCallback(async (username: string) => { |
||||||
try { |
try { |
||||||
// let notificationComments: Item[] =
|
// let notificationComments: Item[] =
|
||||||
// (await notification.getItem('comments')) || []
|
// (await notification.getItem('comments')) || []
|
||||||
// notificationComments = notificationComments
|
// notificationComments = notificationComments
|
||||||
// .filter((nc) => nc.postId && nc.postName && nc.lastSeen)
|
// .filter((nc) => nc.postId && nc.postName && nc.lastSeen)
|
||||||
// .sort((a, b) => b.lastSeen - a.lastSeen)
|
// .sort((a, b) => b.lastSeen - a.lastSeen)
|
||||||
|
|
||||||
const timestamp = await generalLocal.getItem("notification-timestamp"); |
const timestamp = await generalLocal.getItem("notification-timestamp"); |
||||||
|
|
||||||
const after = timestamp || moment().subtract(5, 'days').valueOf(); |
const after = timestamp || moment().subtract(5, "days").valueOf(); |
||||||
|
|
||||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&identifier=${SUPER_LIKE_BASE}&limit=20&includemetadata=true&reverse=true&excludeblocked=true&offset=0&description=${FOR}:${username}_${FOR_SUPER_LIKE}&after=${after}`; |
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&identifier=${SUPER_LIKE_BASE}&limit=20&includemetadata=true&reverse=true&excludeblocked=true&offset=0&description=${FOR}:${username}_${FOR_SUPER_LIKE}&after=${after}`; |
||||||
const response = await fetch(url, { |
const response = await fetch(url, { |
||||||
method: "GET", |
method: "GET", |
||||||
headers: { |
headers: { |
||||||
"Content-Type": "application/json", |
"Content-Type": "application/json", |
||||||
}, |
}, |
||||||
}); |
}); |
||||||
const responseDataSearch = await response.json(); |
const responseDataSearch = await response.json(); |
||||||
let notifys = [] |
let notifys = []; |
||||||
for (const comment of responseDataSearch) { |
for (const comment of responseDataSearch) { |
||||||
if (comment.identifier && comment.name && comment?.metadata?.description) { |
if ( |
||||||
|
comment.identifier && |
||||||
|
comment.name && |
||||||
try { |
comment?.metadata?.description |
||||||
const result = extractSigValue(comment?.metadata?.description) |
) { |
||||||
if(!result) continue |
try { |
||||||
const res = await getPaymentInfo(result); |
const result = extractSigValue(comment?.metadata?.description); |
||||||
if(+res?.amount >= minPriceSuperlike && res.recipient === usernameAddress && isTimestampWithinRange(res?.timestamp, comment.created)){ |
if (!result) continue; |
||||||
|
const res = await getPaymentInfo(result); |
||||||
let urlReference = null |
if ( |
||||||
try { |
+res?.amount >= minPriceSuperlike && |
||||||
let idForUrl = extractIdValue(comment?.metadata?.description) |
res.recipient === usernameAddress && |
||||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${idForUrl}&limit=1&includemetadata=false&reverse=false&excludeblocked=true&offset=0&name=${username}`; |
isTimestampWithinRange(res?.timestamp, comment.created) |
||||||
const response2 = await fetch(url, { |
) { |
||||||
method: "GET", |
let urlReference = null; |
||||||
headers: { |
try { |
||||||
"Content-Type": "application/json", |
let idForUrl = extractIdValue(comment?.metadata?.description); |
||||||
}, |
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${idForUrl}&limit=1&includemetadata=false&reverse=false&excludeblocked=true&offset=0&name=${username}`; |
||||||
}); |
const response2 = await fetch(url, { |
||||||
const responseSearch = await response2.json(); |
method: "GET", |
||||||
if(responseSearch.length > 0){ |
headers: { |
||||||
urlReference = responseSearch[0] |
"Content-Type": "application/json", |
||||||
} |
}, |
||||||
|
}); |
||||||
} catch (error) { |
const responseSearch = await response2.json(); |
||||||
|
if (responseSearch.length > 0) { |
||||||
} |
urlReference = responseSearch[0]; |
||||||
// const url = `/arbitrary/BLOG_COMMENT/${comment.name}/${comment.identifier}`;
|
|
||||||
// const response = await fetch(url, {
|
|
||||||
// method: "GET",
|
|
||||||
// headers: {
|
|
||||||
// "Content-Type": "application/json",
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// if(!response.ok) continue
|
|
||||||
// const responseData2 = await response.text();
|
|
||||||
|
|
||||||
notifys = [...notifys, { |
|
||||||
...comment, |
|
||||||
amount: res.amount, |
|
||||||
urlReference: urlReference || null |
|
||||||
}]; |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
} catch (error) { |
|
||||||
|
|
||||||
} |
} |
||||||
|
} catch (error) {} |
||||||
|
// const url = `/arbitrary/BLOG_COMMENT/${comment.name}/${comment.identifier}`;
|
||||||
|
// const response = await fetch(url, {
|
||||||
|
// method: "GET",
|
||||||
|
// headers: {
|
||||||
|
// "Content-Type": "application/json",
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// if(!response.ok) continue
|
||||||
|
// const responseData2 = await response.text();
|
||||||
|
|
||||||
|
notifys = [ |
||||||
|
...notifys, |
||||||
|
{ |
||||||
|
...comment, |
||||||
|
amount: res.amount, |
||||||
|
urlReference: urlReference || null, |
||||||
|
}, |
||||||
|
]; |
||||||
} |
} |
||||||
} |
} catch (error) {} |
||||||
setNotifications((prev) => { |
|
||||||
const allNotifications = [...notifys, ...prev]; |
|
||||||
const uniqueNotifications = Array.from(new Map(allNotifications.map(notif => [notif.identifier, notif])).values()); |
|
||||||
return uniqueNotifications.slice(0, 20); |
|
||||||
}); |
|
||||||
|
|
||||||
} catch (error) { |
|
||||||
console.log({ error }) |
|
||||||
} |
} |
||||||
}, []) |
} |
||||||
|
setNotifications(prev => { |
||||||
const checkNotificationsFunc = useCallback( |
const allNotifications = [...notifys, ...prev]; |
||||||
(username: string) => { |
const uniqueNotifications = Array.from( |
||||||
let isCalling = false |
new Map( |
||||||
interval.current = setInterval(async () => { |
allNotifications.map(notif => [notif.identifier, notif]) |
||||||
if (isCalling) return |
).values() |
||||||
isCalling = true |
); |
||||||
const res = await checkNotifications(username) |
return uniqueNotifications.slice(0, 20); |
||||||
isCalling = false |
}); |
||||||
}, 60000) |
} catch (error) { |
||||||
checkNotifications(username) |
console.log({ error }); |
||||||
}, |
} |
||||||
[checkNotifications]) |
}, []); |
||||||
|
|
||||||
useEffect(() => { |
const checkNotificationsFunc = useCallback( |
||||||
if (!username) return |
(username: string) => { |
||||||
checkNotificationsFunc(username) |
let isCalling = false; |
||||||
|
interval.current = setInterval(async () => { |
||||||
|
if (isCalling) return; |
||||||
|
isCalling = true; |
||||||
return () => { |
const res = await checkNotifications(username); |
||||||
if (interval?.current) { |
isCalling = false; |
||||||
clearInterval(interval.current) |
}, 60000); |
||||||
} |
checkNotifications(username); |
||||||
} |
}, |
||||||
}, [checkNotificationsFunc, username]) |
[checkNotifications] |
||||||
|
); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!username) return; |
||||||
|
checkNotificationsFunc(username); |
||||||
|
|
||||||
|
return () => { |
||||||
|
if (interval?.current) { |
||||||
|
clearInterval(interval.current); |
||||||
|
} |
||||||
|
}; |
||||||
|
}, [checkNotificationsFunc, username]); |
||||||
|
|
||||||
const openPopover = Boolean(anchorElNotification) |
const openPopover = Boolean(anchorElNotification); |
||||||
return ( |
return ( |
||||||
<Box |
<Box |
||||||
sx={{ |
|
||||||
display: 'flex', |
|
||||||
alignItems: 'center' |
|
||||||
}} |
|
||||||
> |
|
||||||
|
|
||||||
<Badge |
|
||||||
badgeContent={notificationBadgeLength} |
|
||||||
color="primary" |
|
||||||
sx={{ |
sx={{ |
||||||
margin: '0px 12px' |
display: "flex", |
||||||
|
alignItems: "center", |
||||||
}} |
}} |
||||||
> |
> |
||||||
<Button |
<Badge |
||||||
onClick={(e) => { |
badgeContent={notificationBadgeLength} |
||||||
openNotificationPopover(e) |
color="primary" |
||||||
generalLocal.setItem("notification-timestamp", Date.now()); |
|
||||||
setNotificationTimestamp(Date.now) |
|
||||||
}} |
|
||||||
sx={{ |
sx={{ |
||||||
margin: '0px', |
margin: "0px 12px", |
||||||
padding: '0px', |
|
||||||
height: 'auto', |
|
||||||
width: 'auto', |
|
||||||
minWidth: 'unset' |
|
||||||
}} |
}} |
||||||
> |
> |
||||||
<NotificationsIcon color="action" /> |
<Button |
||||||
</Button> |
onClick={e => { |
||||||
</Badge> |
openNotificationPopover(e); |
||||||
<Popover |
generalLocal.setItem("notification-timestamp", Date.now()); |
||||||
id={'simple-popover-notification'} |
setNotificationTimestamp(Date.now); |
||||||
open={openPopover} |
}} |
||||||
anchorEl={anchorElNotification} |
|
||||||
onClose={closeNotificationPopover} |
|
||||||
anchorOrigin={{ |
|
||||||
vertical: 'bottom', |
|
||||||
horizontal: 'left' |
|
||||||
}} |
|
||||||
> |
|
||||||
<Box> |
|
||||||
<List |
|
||||||
sx={{ |
sx={{ |
||||||
maxHeight: '300px', |
margin: "0px", |
||||||
overflow: 'auto' |
padding: "0px", |
||||||
|
height: "auto", |
||||||
|
width: "auto", |
||||||
|
minWidth: "unset", |
||||||
}} |
}} |
||||||
> |
> |
||||||
|
<NotificationsIcon color="action" /> |
||||||
|
</Button> |
||||||
|
</Badge> |
||||||
|
<Popover |
||||||
|
id={"simple-popover-notification"} |
||||||
|
open={openPopover} |
||||||
|
anchorEl={anchorElNotification} |
||||||
|
onClose={closeNotificationPopover} |
||||||
|
anchorOrigin={{ |
||||||
|
vertical: "bottom", |
||||||
|
horizontal: "left", |
||||||
|
}} |
||||||
|
> |
||||||
|
<Box> |
||||||
|
<List |
||||||
|
sx={{ |
||||||
|
maxHeight: "300px", |
||||||
|
overflow: "auto", |
||||||
|
}} |
||||||
|
> |
||||||
{fullNotifications.length === 0 && ( |
{fullNotifications.length === 0 && ( |
||||||
<ListItem |
<ListItem> |
||||||
|
<ListItemText primary="No new notifications"></ListItemText> |
||||||
|
</ListItem> |
||||||
|
)} |
||||||
|
{fullNotifications.map((notification: any, index: number) => ( |
||||||
|
<ListItem |
||||||
|
key={index} |
||||||
|
divider |
||||||
|
sx={{ |
||||||
|
cursor: notification?.urlReference ? "pointer" : "default", |
||||||
|
}} |
||||||
|
onClick={async () => { |
||||||
|
if (notification?.urlReference) { |
||||||
|
navigate( |
||||||
|
`/video/${notification?.urlReference?.name}/${notification?.urlReference?.identifier}` |
||||||
|
); |
||||||
|
} |
||||||
|
}} |
||||||
> |
> |
||||||
<ListItemText |
<ListItemText |
||||||
primary="No new notifications"> |
primary={ |
||||||
|
<Box |
||||||
</ListItemText> |
|
||||||
</ListItem> |
|
||||||
)} |
|
||||||
{fullNotifications.map((notification: any, index: number) => ( |
|
||||||
<ListItem |
|
||||||
key={index} |
|
||||||
divider |
|
||||||
sx={{ |
|
||||||
cursor: notification?.urlReference ? 'pointer' : 'default' |
|
||||||
}} |
|
||||||
onClick={async () => { |
|
||||||
if(notification?.urlReference){ |
|
||||||
navigate(`/video/${notification?.urlReference?.name}/${notification?.urlReference?.identifier}`); |
|
||||||
} |
|
||||||
}} |
|
||||||
> |
|
||||||
<ListItemText |
|
||||||
primary={ |
|
||||||
<Box sx={{ |
|
||||||
display: 'flex', |
|
||||||
alignItems: 'center', |
|
||||||
gap: '5px' |
|
||||||
}}> |
|
||||||
<Typography |
|
||||||
component="span" |
|
||||||
variant="body1" |
|
||||||
color="textPrimary" |
|
||||||
> |
|
||||||
Super Like |
|
||||||
|
|
||||||
</Typography> |
|
||||||
<ThumbUpIcon |
|
||||||
style={{ |
|
||||||
color: "gold", |
|
||||||
|
|
||||||
|
|
||||||
}} |
|
||||||
/> |
|
||||||
</Box> |
|
||||||
} |
|
||||||
secondary={ |
|
||||||
<React.Fragment> |
|
||||||
<Typography |
|
||||||
component="span" |
|
||||||
sx={{ |
|
||||||
fontSize: '16px' |
|
||||||
}} |
|
||||||
color="textSecondary" |
|
||||||
> |
|
||||||
{formatDate(notification.created)} |
|
||||||
</Typography> |
|
||||||
<Typography |
|
||||||
component="span" |
|
||||||
sx={{ |
sx={{ |
||||||
fontSize: '16px' |
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
gap: "5px", |
||||||
}} |
}} |
||||||
color="textSecondary" |
|
||||||
> |
> |
||||||
{` from ${notification.name}`} |
<Typography |
||||||
</Typography> |
component="span" |
||||||
</React.Fragment> |
variant="body1" |
||||||
} |
color="textPrimary" |
||||||
/> |
> |
||||||
</ListItem> |
Super Like |
||||||
))} |
</Typography> |
||||||
</List> |
<ThumbUpIcon |
||||||
</Box> |
style={{ |
||||||
</Popover> |
color: "gold", |
||||||
</Box> |
}} |
||||||
) |
/> |
||||||
} |
</Box> |
||||||
|
} |
||||||
|
secondary={ |
||||||
|
<React.Fragment> |
||||||
|
<Typography |
||||||
|
component="span" |
||||||
|
sx={{ |
||||||
|
fontSize: "16px", |
||||||
|
}} |
||||||
|
color="textSecondary" |
||||||
|
> |
||||||
|
{formatDate(notification.created)} |
||||||
|
</Typography> |
||||||
|
<Typography |
||||||
|
component="span" |
||||||
|
sx={{ |
||||||
|
fontSize: "16px", |
||||||
|
}} |
||||||
|
color="textSecondary" |
||||||
|
> |
||||||
|
{` from ${notification.name}`} |
||||||
|
</Typography> |
||||||
|
</React.Fragment> |
||||||
|
} |
||||||
|
/> |
||||||
|
</ListItem> |
||||||
|
))} |
||||||
|
</List> |
||||||
|
</Box> |
||||||
|
</Popover> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
||||||
|
@ -0,0 +1,107 @@ |
|||||||
|
interface SubCategory { |
||||||
|
id: number; |
||||||
|
name: string; |
||||||
|
} |
||||||
|
|
||||||
|
interface CategoryMap { |
||||||
|
[key: number]: SubCategory[]; |
||||||
|
} |
||||||
|
|
||||||
|
const sortCategory = (a: SubCategory, b: SubCategory) => { |
||||||
|
if (a.name === "Other") return 1; |
||||||
|
else if (b.name === "Other") return -1; |
||||||
|
else return a.name.localeCompare(b.name); |
||||||
|
}; |
||||||
|
export const categories = [ |
||||||
|
{ id: 1, name: "Movies" }, |
||||||
|
{ id: 2, name: "Series" }, |
||||||
|
{ id: 3, name: "Music" }, |
||||||
|
{ id: 4, name: "Education" }, |
||||||
|
{ id: 5, name: "Lifestyle" }, |
||||||
|
{ id: 6, name: "Gaming" }, |
||||||
|
{ id: 7, name: "Technology" }, |
||||||
|
{ id: 8, name: "Sports" }, |
||||||
|
{ id: 9, name: "News & Politics" }, |
||||||
|
{ id: 10, name: "Cooking & Food" }, |
||||||
|
{ id: 11, name: "Animation" }, |
||||||
|
{ id: 12, name: "Science" }, |
||||||
|
{ id: 13, name: "Health & Wellness" }, |
||||||
|
{ id: 14, name: "DIY & Crafts" }, |
||||||
|
{ id: 15, name: "Kids & Family" }, |
||||||
|
{ id: 16, name: "Comedy" }, |
||||||
|
{ id: 17, name: "Travel & Adventure" }, |
||||||
|
{ id: 18, name: "Art & Design" }, |
||||||
|
{ id: 19, name: "Nature & Environment" }, |
||||||
|
{ id: 20, name: "Business & Finance" }, |
||||||
|
{ id: 21, name: "Personal Development" }, |
||||||
|
{ id: 22, name: "Other" }, |
||||||
|
{ id: 23, name: "History" }, |
||||||
|
{ id: 24, name: "Anime" }, |
||||||
|
{ id: 25, name: "Cartoons" }, |
||||||
|
{ id: 26, name: "Qortal" }, |
||||||
|
].sort(sortCategory); |
||||||
|
|
||||||
|
export const subCategories: CategoryMap = { |
||||||
|
1: [ |
||||||
|
// Movies
|
||||||
|
{ id: 101, name: "Action & Adventure" }, |
||||||
|
{ id: 102, name: "Comedy" }, |
||||||
|
{ id: 103, name: "Drama" }, |
||||||
|
{ id: 104, name: "Fantasy & Science Fiction" }, |
||||||
|
{ id: 105, name: "Horror & Thriller" }, |
||||||
|
{ id: 106, name: "Documentaries" }, |
||||||
|
{ id: 107, name: "Animated" }, |
||||||
|
{ id: 108, name: "Family & Kids" }, |
||||||
|
{ id: 109, name: "Romance" }, |
||||||
|
{ id: 110, name: "Mystery & Crime" }, |
||||||
|
{ id: 111, name: "Historical & War" }, |
||||||
|
{ id: 112, name: "Musicals & Music Films" }, |
||||||
|
{ id: 113, name: "Indie Films" }, |
||||||
|
{ id: 114, name: "International Films" }, |
||||||
|
{ id: 115, name: "Biographies & True Stories" }, |
||||||
|
{ id: 116, name: "Other" }, |
||||||
|
].sort(sortCategory), |
||||||
|
2: [ |
||||||
|
// Series
|
||||||
|
{ id: 201, name: "Dramas" }, |
||||||
|
{ id: 202, name: "Comedies" }, |
||||||
|
{ id: 203, name: "Reality & Competition" }, |
||||||
|
{ id: 204, name: "Documentaries & Docuseries" }, |
||||||
|
{ id: 205, name: "Sci-Fi & Fantasy" }, |
||||||
|
{ id: 206, name: "Crime & Mystery" }, |
||||||
|
{ id: 207, name: "Animated Series" }, |
||||||
|
{ id: 208, name: "Kids & Family" }, |
||||||
|
{ id: 209, name: "Historical & Period Pieces" }, |
||||||
|
{ id: 210, name: "Action & Adventure" }, |
||||||
|
{ id: 211, name: "Horror & Thriller" }, |
||||||
|
{ id: 212, name: "Romance" }, |
||||||
|
{ id: 213, name: "Anthologies" }, |
||||||
|
{ id: 214, name: "International Series" }, |
||||||
|
{ id: 215, name: "Miniseries" }, |
||||||
|
{ id: 216, name: "Other" }, |
||||||
|
].sort(sortCategory), |
||||||
|
4: [ |
||||||
|
// Education
|
||||||
|
{ id: 400, name: "Tutorial" }, |
||||||
|
{ id: 401, name: "Documentary" }, |
||||||
|
{ id: 401, name: "Qortal" }, |
||||||
|
{ id: 402, name: "Other" }, |
||||||
|
].sort(sortCategory), |
||||||
|
|
||||||
|
24: [ |
||||||
|
{ id: 2401, name: "Kodomomuke" }, |
||||||
|
{ id: 2402, name: "Shonen" }, |
||||||
|
{ id: 2403, name: "Shoujo" }, |
||||||
|
{ id: 2404, name: "Seinen" }, |
||||||
|
{ id: 2405, name: "Josei" }, |
||||||
|
{ id: 2406, name: "Mecha" }, |
||||||
|
{ id: 2407, name: "Mahou Shoujo" }, |
||||||
|
{ id: 2408, name: "Isekai" }, |
||||||
|
{ id: 2409, name: "Yaoi" }, |
||||||
|
{ id: 2410, name: "Yuri" }, |
||||||
|
{ id: 2411, name: "Harem" }, |
||||||
|
{ id: 2412, name: "Ecchi" }, |
||||||
|
{ id: 2413, name: "Idol" }, |
||||||
|
{ id: 2414, name: "Other" }, |
||||||
|
].sort(sortCategory), |
||||||
|
}; |
@ -0,0 +1,15 @@ |
|||||||
|
const useTestIdentifiers = false; |
||||||
|
export const QTUBE_VIDEO_BASE = useTestIdentifiers |
||||||
|
? "MYTEST_vid_" |
||||||
|
: "qtube_vid_"; |
||||||
|
export const QTUBE_PLAYLIST_BASE = useTestIdentifiers |
||||||
|
? "MYTEST_playlist_" |
||||||
|
: "qtube_playlist_"; |
||||||
|
export const SUPER_LIKE_BASE = useTestIdentifiers |
||||||
|
? "MYTEST_superlike_" |
||||||
|
: "qtube_superlike_"; |
||||||
|
export const COMMENT_BASE = useTestIdentifiers |
||||||
|
? "qcomment_v1_MYTEST_" |
||||||
|
: "qcomment_v1_qtube_"; |
||||||
|
export const FOR = useTestIdentifiers ? "FORTEST5" : "FOR0962"; |
||||||
|
export const FOR_SUPER_LIKE = useTestIdentifiers ? "MYTEST_sl" : `qtube_sl`; |
@ -0,0 +1,2 @@ |
|||||||
|
export const minPriceSuperlike = 10; |
||||||
|
export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.;:|—~@#$%^&*+=]/g; |
@ -1,121 +0,0 @@ |
|||||||
const useTestIdentifiers = true; |
|
||||||
|
|
||||||
export const QTUBE_VIDEO_BASE = useTestIdentifiers |
|
||||||
? "MYTEST_vid_" |
|
||||||
: "qtube_vid_"; |
|
||||||
|
|
||||||
export const QTUBE_PLAYLIST_BASE = useTestIdentifiers |
|
||||||
? "MYTEST_playlist_" |
|
||||||
: "qtube_playlist_"; |
|
||||||
|
|
||||||
export const SUPER_LIKE_BASE = useTestIdentifiers |
|
||||||
? "MYTEST_superlike_" |
|
||||||
: "qtube_superlike_"; |
|
||||||
|
|
||||||
export const COMMENT_BASE = useTestIdentifiers |
|
||||||
? "qcomment_v1_MYTEST_" |
|
||||||
: "qcomment_v1_qtube_"; |
|
||||||
|
|
||||||
export const FOR = useTestIdentifiers |
|
||||||
? "FORTEST5" |
|
||||||
: "FOR0962"; |
|
||||||
|
|
||||||
export const FOR_SUPER_LIKE = useTestIdentifiers |
|
||||||
? "MYTEST_sl" |
|
||||||
: `qtube_sl`; |
|
||||||
|
|
||||||
export const minPriceSuperlike = 10 |
|
||||||
|
|
||||||
interface SubCategory { |
|
||||||
id: number; |
|
||||||
name: string; |
|
||||||
} |
|
||||||
|
|
||||||
interface CategoryMap { |
|
||||||
[key: number]: SubCategory[]; |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
export const categories = [ |
|
||||||
{"id": 1, "name": "Movies"}, |
|
||||||
{"id": 2, "name": "Series"}, |
|
||||||
{"id": 3, "name": "Music"}, |
|
||||||
{"id": 4, "name": "Education"}, |
|
||||||
{"id": 5, "name": "Lifestyle"}, |
|
||||||
{"id": 6, "name": "Gaming"}, |
|
||||||
{"id": 7, "name": "Technology"}, |
|
||||||
{"id": 8, "name": "Sports"}, |
|
||||||
{"id": 9, "name": "News & Politics"}, |
|
||||||
{"id": 10, "name": "Cooking & Food"}, |
|
||||||
{"id": 11, "name": "Animation"}, |
|
||||||
{"id": 12, "name": "Science"}, |
|
||||||
{"id": 13, "name": "Health & Wellness"}, |
|
||||||
{"id": 14, "name": "DIY & Crafts"}, |
|
||||||
{"id": 15, "name": "Kids & Family"}, |
|
||||||
{"id": 16, "name": "Comedy"}, |
|
||||||
{"id": 17, "name": "Travel & Adventure"}, |
|
||||||
{"id": 18, "name": "Art & Design"}, |
|
||||||
{"id": 19, "name": "Nature & Environment"}, |
|
||||||
{"id": 20, "name": "Business & Finance"}, |
|
||||||
{"id": 21, "name": "Personal Development"}, |
|
||||||
{"id": 22, "name": "Other"}, |
|
||||||
{"id": 23, "name": "History"}, |
|
||||||
{"id": 24, "name": "Anime"}, |
|
||||||
{"id": 25, "name": "Cartoons"} |
|
||||||
] |
|
||||||
|
|
||||||
|
|
||||||
export const subCategories: CategoryMap = { |
|
||||||
1: [ // Movies
|
|
||||||
{"id": 101, "name": "Action & Adventure"}, |
|
||||||
{"id": 102, "name": "Comedy"}, |
|
||||||
{"id": 103, "name": "Drama"}, |
|
||||||
{"id": 104, "name": "Fantasy & Science Fiction"}, |
|
||||||
{"id": 105, "name": "Horror & Thriller"}, |
|
||||||
{"id": 106, "name": "Documentaries"}, |
|
||||||
{"id": 107, "name": "Animated"}, |
|
||||||
{"id": 108, "name": "Family & Kids"}, |
|
||||||
{"id": 109, "name": "Romance"}, |
|
||||||
{"id": 110, "name": "Mystery & Crime"}, |
|
||||||
{"id": 111, "name": "Historical & War"}, |
|
||||||
{"id": 112, "name": "Musicals & Music Films"}, |
|
||||||
{"id": 113, "name": "Indie Films"}, |
|
||||||
{"id": 114, "name": "International Films"}, |
|
||||||
{"id": 115, "name": "Biographies & True Stories"}, |
|
||||||
{"id": 116, "name": "Other"} |
|
||||||
], |
|
||||||
2: [ // Series
|
|
||||||
{"id": 201, "name": "Dramas"}, |
|
||||||
{"id": 202, "name": "Comedies"}, |
|
||||||
{"id": 203, "name": "Reality & Competition"}, |
|
||||||
{"id": 204, "name": "Documentaries & Docuseries"}, |
|
||||||
{"id": 205, "name": "Sci-Fi & Fantasy"}, |
|
||||||
{"id": 206, "name": "Crime & Mystery"}, |
|
||||||
{"id": 207, "name": "Animated Series"}, |
|
||||||
{"id": 208, "name": "Kids & Family"}, |
|
||||||
{"id": 209, "name": "Historical & Period Pieces"}, |
|
||||||
{"id": 210, "name": "Action & Adventure"}, |
|
||||||
{"id": 211, "name": "Horror & Thriller"}, |
|
||||||
{"id": 212, "name": "Romance"}, |
|
||||||
{"id": 213, "name": "Anthologies"}, |
|
||||||
{"id": 214, "name": "International Series"}, |
|
||||||
{"id": 215, "name": "Miniseries"}, |
|
||||||
{"id": 216, "name": "Other"} |
|
||||||
], |
|
||||||
24: [ |
|
||||||
{"id": 2401, "name": "Kodomomuke"}, |
|
||||||
{"id": 2402, "name": "Shonen"}, |
|
||||||
{"id": 2403, "name": "Shoujo"}, |
|
||||||
{"id": 2404, "name": "Seinen"}, |
|
||||||
{"id": 2405, "name": "Josei"}, |
|
||||||
{"id": 2406, "name": "Mecha"}, |
|
||||||
{"id": 2407, "name": "Mahou Shoujo"}, |
|
||||||
{"id": 2408, "name": "Isekai"}, |
|
||||||
{"id": 2409, "name": "Yaoi"}, |
|
||||||
{"id": 2410, "name": "Yuri"}, |
|
||||||
{"id": 2411, "name": "Harem"}, |
|
||||||
{"id": 2412, "name": "Ecchi"}, |
|
||||||
{"id": 2413, "name": "Idol"}, |
|
||||||
{"id": 2414, "name": "Other"} |
|
||||||
]
|
|
||||||
} |
|
@ -0,0 +1,154 @@ |
|||||||
|
import { |
||||||
|
IconButton, |
||||||
|
InputAdornment, |
||||||
|
TextField, |
||||||
|
TextFieldProps, |
||||||
|
} from "@mui/material"; |
||||||
|
import React, { useRef, useState } from "react"; |
||||||
|
import AddIcon from "@mui/icons-material/Add"; |
||||||
|
import RemoveIcon from "@mui/icons-material/Remove"; |
||||||
|
import { |
||||||
|
removeTrailingZeros, |
||||||
|
setNumberWithinBounds, |
||||||
|
} from "./numberFunctions.ts"; |
||||||
|
|
||||||
|
type eventType = React.ChangeEvent<HTMLInputElement>; |
||||||
|
type BoundedNumericTextFieldProps = { |
||||||
|
minValue: number; |
||||||
|
maxValue: number; |
||||||
|
addIconButtons?: boolean; |
||||||
|
allowDecimals?: boolean; |
||||||
|
allowNegatives?: boolean; |
||||||
|
afterChange?: (s: string) => void; |
||||||
|
initialValue?: string; |
||||||
|
maxSigDigits?: number; |
||||||
|
} & TextFieldProps; |
||||||
|
|
||||||
|
export const BoundedNumericTextField = ({ |
||||||
|
minValue, |
||||||
|
maxValue, |
||||||
|
addIconButtons = true, |
||||||
|
allowDecimals = true, |
||||||
|
allowNegatives = false, |
||||||
|
afterChange, |
||||||
|
initialValue, |
||||||
|
maxSigDigits = 6, |
||||||
|
...props |
||||||
|
}: BoundedNumericTextFieldProps) => { |
||||||
|
const [textFieldValue, setTextFieldValue] = useState<string>( |
||||||
|
initialValue || "" |
||||||
|
); |
||||||
|
const ref = useRef<HTMLInputElement | null>(null); |
||||||
|
|
||||||
|
const stringIsEmpty = (value: string) => { |
||||||
|
return value === ""; |
||||||
|
}; |
||||||
|
const isAllZerosNum = /^0*\.?0*$/; |
||||||
|
const isFloatNum = /^-?[0-9]*\.?[0-9]*$/; |
||||||
|
const isIntegerNum = /^-?[0-9]+$/; |
||||||
|
const skipMinMaxCheck = (value: string) => { |
||||||
|
const lastIndexIsDecimal = value.charAt(value.length - 1) === "."; |
||||||
|
const isEmpty = stringIsEmpty(value); |
||||||
|
const isAllZeros = isAllZerosNum.test(value); |
||||||
|
const isInteger = isIntegerNum.test(value); |
||||||
|
// skipping minMax on all 0s allows values less than 1 to be entered
|
||||||
|
|
||||||
|
return lastIndexIsDecimal || isEmpty || (isAllZeros && !isInteger); |
||||||
|
}; |
||||||
|
|
||||||
|
const setMinMaxValue = (value: string): string => { |
||||||
|
if (skipMinMaxCheck(value)) return value; |
||||||
|
const valueNum = Number(value); |
||||||
|
|
||||||
|
const boundedNum = setNumberWithinBounds(valueNum, minValue, maxValue); |
||||||
|
|
||||||
|
const numberInBounds = boundedNum === valueNum; |
||||||
|
return numberInBounds ? value : boundedNum.toString(); |
||||||
|
}; |
||||||
|
|
||||||
|
const getSigDigits = (number: string) => { |
||||||
|
if (isIntegerNum.test(number)) return 0; |
||||||
|
const decimalSplit = number.split("."); |
||||||
|
return decimalSplit[decimalSplit.length - 1].length; |
||||||
|
}; |
||||||
|
|
||||||
|
const sigDigitsExceeded = (number: string, sigDigits: number) => { |
||||||
|
return getSigDigits(number) > sigDigits; |
||||||
|
}; |
||||||
|
|
||||||
|
const filterTypes = (value: string) => { |
||||||
|
if (allowDecimals === false) value = value.replace(".", ""); |
||||||
|
if (allowNegatives === false) value = value.replace("-", ""); |
||||||
|
if (sigDigitsExceeded(value, maxSigDigits)) { |
||||||
|
value = value.substring(0, value.length - 1); |
||||||
|
} |
||||||
|
return value; |
||||||
|
}; |
||||||
|
const filterValue = (value: string) => { |
||||||
|
if (stringIsEmpty(value)) return ""; |
||||||
|
value = filterTypes(value); |
||||||
|
if (isFloatNum.test(value)) { |
||||||
|
return setMinMaxValue(value); |
||||||
|
} |
||||||
|
return textFieldValue; |
||||||
|
}; |
||||||
|
|
||||||
|
const listeners = (e: eventType) => { |
||||||
|
// console.log("changeEvent:", e);
|
||||||
|
const newValue = filterValue(e.target.value); |
||||||
|
setTextFieldValue(newValue); |
||||||
|
if (afterChange) afterChange(newValue); |
||||||
|
}; |
||||||
|
|
||||||
|
const changeValueWithIncDecButton = (changeAmount: number) => { |
||||||
|
const changedValue = (+textFieldValue + changeAmount).toString(); |
||||||
|
const inBoundsValue = setMinMaxValue(changedValue); |
||||||
|
setTextFieldValue(inBoundsValue); |
||||||
|
if (afterChange) afterChange(inBoundsValue); |
||||||
|
}; |
||||||
|
|
||||||
|
const formatValueOnBlur = (e: eventType) => { |
||||||
|
let value = e.target.value; |
||||||
|
if (stringIsEmpty(value) || value === ".") { |
||||||
|
setTextFieldValue(""); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
value = setMinMaxValue(value); |
||||||
|
value = removeTrailingZeros(value); |
||||||
|
if (isAllZerosNum.test(value)) value = minValue.toString(); |
||||||
|
|
||||||
|
setTextFieldValue(value); |
||||||
|
}; |
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { onChange, ...noChangeProps } = { ...props }; |
||||||
|
return ( |
||||||
|
<TextField |
||||||
|
{...noChangeProps} |
||||||
|
InputProps={{ |
||||||
|
...props?.InputProps, |
||||||
|
endAdornment: addIconButtons ? ( |
||||||
|
<InputAdornment position="end"> |
||||||
|
<IconButton onClick={() => changeValueWithIncDecButton(1)}> |
||||||
|
<AddIcon />{" "} |
||||||
|
</IconButton> |
||||||
|
<IconButton onClick={() => changeValueWithIncDecButton(-1)}> |
||||||
|
<RemoveIcon />{" "} |
||||||
|
</IconButton> |
||||||
|
</InputAdornment> |
||||||
|
) : ( |
||||||
|
<></> |
||||||
|
), |
||||||
|
}} |
||||||
|
onChange={e => listeners(e as eventType)} |
||||||
|
onBlur={e => { |
||||||
|
formatValueOnBlur(e as eventType); |
||||||
|
}} |
||||||
|
autoComplete="off" |
||||||
|
value={textFieldValue} |
||||||
|
inputRef={ref} |
||||||
|
/> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default BoundedNumericTextField; |
@ -0,0 +1,28 @@ |
|||||||
|
import * as colorsys from "colorsys"; |
||||||
|
|
||||||
|
export const truncateNumber = (value: string | number, sigDigits: number) => { |
||||||
|
return Number(value).toFixed(sigDigits); |
||||||
|
}; |
||||||
|
|
||||||
|
export const changeLightness = (hexColor: string, amount: number) => { |
||||||
|
const hsl = colorsys.hex2Hsl(hexColor); |
||||||
|
hsl.l += amount; |
||||||
|
return colorsys.hsl2Hex(hsl); |
||||||
|
}; |
||||||
|
export const removeTrailingZeros = (s: string) => { |
||||||
|
return Number(s).toString(); |
||||||
|
}; |
||||||
|
|
||||||
|
export const setNumberWithinBounds = ( |
||||||
|
num: number, |
||||||
|
minValue: number, |
||||||
|
maxValue: number |
||||||
|
) => { |
||||||
|
if (num > maxValue) return maxValue; |
||||||
|
if (num < minValue) return minValue; |
||||||
|
return num; |
||||||
|
}; |
||||||
|
|
||||||
|
export const numberToInt = (num: number) => { |
||||||
|
return Math.floor(num); |
||||||
|
}; |
@ -0,0 +1,48 @@ |
|||||||
|
import { |
||||||
|
AccountInfo, |
||||||
|
AccountName, |
||||||
|
GetRequestData, |
||||||
|
SearchTransactionResponse, |
||||||
|
TransactionSearchParams, |
||||||
|
} from "./qortalRequestTypes.ts"; |
||||||
|
|
||||||
|
export const getBalance = async (address: string) => { |
||||||
|
return (await qortalRequest({ |
||||||
|
action: "GET_BALANCE", |
||||||
|
address, |
||||||
|
})) as number; |
||||||
|
}; |
||||||
|
|
||||||
|
export const getUserAccount = async () => { |
||||||
|
return (await qortalRequest({ |
||||||
|
action: "GET_USER_ACCOUNT", |
||||||
|
})) as AccountInfo; |
||||||
|
}; |
||||||
|
export const getUserBalance = async () => { |
||||||
|
const accountInfo = await getUserAccount(); |
||||||
|
return (await getBalance(accountInfo.address)) as number; |
||||||
|
}; |
||||||
|
export const getAccountNames = async ( |
||||||
|
address: string, |
||||||
|
params?: GetRequestData |
||||||
|
) => { |
||||||
|
const names = (await qortalRequest({ |
||||||
|
action: "GET_ACCOUNT_NAMES", |
||||||
|
address, |
||||||
|
...params, |
||||||
|
})) as AccountName[]; |
||||||
|
|
||||||
|
const namelessAddress = { name: "", owner: address }; |
||||||
|
const emptyNamesFilled = names.map(({ name, owner }) => { |
||||||
|
return name ? { name, owner } : namelessAddress; |
||||||
|
}); |
||||||
|
|
||||||
|
return emptyNamesFilled.length > 0 ? emptyNamesFilled : [namelessAddress]; |
||||||
|
}; |
||||||
|
|
||||||
|
export const searchTransactions = async (params: TransactionSearchParams) => { |
||||||
|
return (await qortalRequest({ |
||||||
|
action: "SEARCH_TRANSACTIONS", |
||||||
|
...params, |
||||||
|
})) as SearchTransactionResponse[]; |
||||||
|
}; |
@ -0,0 +1,76 @@ |
|||||||
|
export type AccountInfo = { address: string; publicKey: string }; |
||||||
|
export type AccountName = { name: string; owner: string }; |
||||||
|
export type ConfirmationStatus = "CONFIRMED" | "UNCONFIRMED" | "BOTH"; |
||||||
|
|
||||||
|
export interface GetRequestData { |
||||||
|
limit?: number; |
||||||
|
offset?: number; |
||||||
|
reverse?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export interface SearchTransactionResponse { |
||||||
|
type: string; |
||||||
|
timestamp: number; |
||||||
|
reference: string; |
||||||
|
fee: string; |
||||||
|
signature: string; |
||||||
|
txGroupId: number; |
||||||
|
blockHeight: number; |
||||||
|
approvalStatus: string; |
||||||
|
creatorAddress: string; |
||||||
|
senderPublicKey: string; |
||||||
|
recipient: string; |
||||||
|
amount: string; |
||||||
|
} |
||||||
|
|
||||||
|
export type TransactionType = |
||||||
|
| "GENESIS" |
||||||
|
| "PAYMENT" |
||||||
|
| "REGISTER_NAME" |
||||||
|
| "UPDATE_NAME" |
||||||
|
| "SELL_NAME" |
||||||
|
| "CANCEL_SELL_NAME" |
||||||
|
| "BUY_NAME" |
||||||
|
| "CREATE_POLL" |
||||||
|
| "VOTE_ON_POLL" |
||||||
|
| "ARBITRARY" |
||||||
|
| "ISSUE_ASSET" |
||||||
|
| "TRANSFER_ASSET" |
||||||
|
| "CREATE_ASSET_ORDER" |
||||||
|
| "CANCEL_ASSET_ORDER" |
||||||
|
| "MULTI_PAYMENT" |
||||||
|
| "DEPLOY_AT" |
||||||
|
| "MESSAGE" |
||||||
|
| "CHAT" |
||||||
|
| "PUBLICIZE" |
||||||
|
| "AIRDROP" |
||||||
|
| "AT" |
||||||
|
| "CREATE_GROUP" |
||||||
|
| "UPDATE_GROUP" |
||||||
|
| "ADD_GROUP_ADMIN" |
||||||
|
| "REMOVE_GROUP_ADMIN" |
||||||
|
| "GROUP_BAN" |
||||||
|
| "CANCEL_GROUP_BAN" |
||||||
|
| "GROUP_KICK" |
||||||
|
| "GROUP_INVITE" |
||||||
|
| "CANCEL_GROUP_INVITE" |
||||||
|
| "JOIN_GROUP" |
||||||
|
| "LEAVE_GROUP" |
||||||
|
| "GROUP_APPROVAL" |
||||||
|
| "SET_GROUP" |
||||||
|
| "UPDATE_ASSET" |
||||||
|
| "ACCOUNT_FLAGS" |
||||||
|
| "ENABLE_FORGING" |
||||||
|
| "REWARD_SHARE" |
||||||
|
| "ACCOUNT_LEVEL" |
||||||
|
| "TRANSFER_PRIVS" |
||||||
|
| "PRESENCE"; |
||||||
|
|
||||||
|
export interface TransactionSearchParams extends GetRequestData { |
||||||
|
startBlock?: number; |
||||||
|
blockLimit?: number; |
||||||
|
txGroupId?: number; |
||||||
|
txType: TransactionType[]; |
||||||
|
address: string; |
||||||
|
confirmationStatus: ConfirmationStatus; |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
export const getFileExtensionIndex = (s: string) => { |
||||||
|
const lastIndex = s.lastIndexOf("."); |
||||||
|
return lastIndex > 0 ? lastIndex : s.length - 1; |
||||||
|
}; |
||||||
|
|
||||||
|
export const getFileName = (s: string) => { |
||||||
|
return s.substring(0, getFileExtensionIndex(s)); |
||||||
|
}; |
Loading…
Reference in new issue