diff --git a/package.json b/package.json index daefc502..8180d73e 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "rsc": "node scripts/release-sanity-check.mjs" }, "dependencies": { + "@distributedlab/jac": "^1.0.0-rc.9", "@distributedlab/tools": "^1.0.0-rc.9", "@distributedlab/w3p": "^1.0.0-rc.9", "@emotion/react": "^11.11.1", diff --git a/src/api/clients/index.ts b/src/api/clients/index.ts new file mode 100644 index 00000000..9b3b4e50 --- /dev/null +++ b/src/api/clients/index.ts @@ -0,0 +1,6 @@ +import { config } from '@config' +import { JsonApiClient } from '@distributedlab/jac' + +export const api = new JsonApiClient({ + baseUrl: config.API_URL, +}) diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 00000000..2e298073 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,2 @@ +export * from './clients' +export * from './modules' diff --git a/src/api/modules/index.ts b/src/api/modules/index.ts new file mode 100644 index 00000000..fbf7cbbd --- /dev/null +++ b/src/api/modules/index.ts @@ -0,0 +1 @@ +export * from './orgs' diff --git a/src/api/modules/orgs/enums.ts b/src/api/modules/orgs/enums.ts new file mode 100644 index 00000000..3d1d00da --- /dev/null +++ b/src/api/modules/orgs/enums.ts @@ -0,0 +1,15 @@ +export enum OrgsStatuses { + Unverified = '0', + Verified = '1', +} + +export enum OrgsRequestFilters { + Owner = 'owner', + UserDid = 'user_did', + Status = 'status', +} + +export enum OrgsIncludes { + organization = 'organization', + owner = 'owner', +} diff --git a/src/api/modules/orgs/helpers.ts b/src/api/modules/orgs/helpers.ts new file mode 100644 index 00000000..1726e474 --- /dev/null +++ b/src/api/modules/orgs/helpers.ts @@ -0,0 +1,54 @@ +import { + api, + type Organization, + type OrganizationCreate, + type OrgsRequestQueryParams, + type OrgUser, + type OrgVerificationCode, +} from '@/api' + +export const loadOrgs = async (query: OrgsRequestQueryParams) => { + return api.get('/v1/orgs', { + query, + }) +} + +export const loadOrgsAmount = async () => { + return api.get('/v1/orgs/amount') +} + +export const loadOrgById = async (id: string, query: OrgsRequestQueryParams) => { + return api.get(`/v1/orgs/${id}`, { + query, + }) +} + +export const createOrg = async (body: OrganizationCreate) => { + return api.post('/v1/orgs', { + body: { + data: { + id: body.id, + type: 'organizations-create', + attributes: { + owner_did: body.ownerDid, + domain: body.domain, + metadata: body.metadata, + }, + }, + }, + }) +} + +export const verifyOrg = async (id: string) => { + return api.post(`/v1/orgs/${id}`) +} + +export const loadOrgUsers = async (id: string, query: OrgsRequestQueryParams) => { + return api.get(`/v1/orgs/${id}/users`, { + query, + }) +} + +export const getOrgVerificationCode = async (id: string) => { + return api.get(`/v1/orgs/${id}/verification-code`) +} diff --git a/src/api/modules/orgs/index.ts b/src/api/modules/orgs/index.ts new file mode 100644 index 00000000..b6c869a7 --- /dev/null +++ b/src/api/modules/orgs/index.ts @@ -0,0 +1,3 @@ +export * from './enums' +export * from './helpers' +export * from './types' diff --git a/src/api/modules/orgs/types.ts b/src/api/modules/orgs/types.ts new file mode 100644 index 00000000..619a0096 --- /dev/null +++ b/src/api/modules/orgs/types.ts @@ -0,0 +1,62 @@ +import type { OrgsIncludes, OrgsRequestFilters, OrgsStatuses } from '@/api' + +export type OrgMetadata = { + name: string + description: string +} + +export type OrgUser = { + id: string + type: 'users' + did: string + role: { + name: string + value: number + } + org_id: string + created_at: string + updated_at: string +} + +export type Organization = { + id: string + type: 'organizations' + did: string + owner?: OrgUser + domain: string + metadata: OrgMetadata + status: { + name: string + value: OrgsStatuses + } + verification_code: string + issued_claims_count: string + members_count: string + created_at: string + updated_at: string +} + +export type OrganizationCreate = { + id: string + ownerDid: string + domain: string + metadata: OrgMetadata +} + +export type OrgsRequestFiltersMap = { + [OrgsRequestFilters.Owner]?: string + [OrgsRequestFilters.UserDid]?: string + [OrgsRequestFilters.Status]?: OrgsStatuses +} + +export type OrgsRequestQueryParams = { + include?: OrgsIncludes + filter?: OrgsRequestFiltersMap + // TODO: page, limit, sort, ...etc +} + +export type OrgVerificationCode = { + id: string + type: string + code: string +} diff --git a/src/common/AppNavbar.tsx b/src/common/AppNavbar.tsx new file mode 100644 index 00000000..eaea37a6 --- /dev/null +++ b/src/common/AppNavbar.tsx @@ -0,0 +1,66 @@ +import { config } from '@config' +import { ButtonProps, Divider, Stack } from '@mui/material' +import { ReactNode, useMemo } from 'react' +import { NavLink, useLocation } from 'react-router-dom' + +import { Icons, Routes } from '@/enums' +import { UiButton, UiIcon } from '@/ui' + +interface NavbarLinkProps { + to: Routes + children: ReactNode +} + +const NavbarLink = ({ children, to }: NavbarLinkProps) => { + const location = useLocation() + + const linkProps = useMemo((): Partial => { + const locationRoot = location.pathname.split('/')[1] + + const isRouteActive = to.includes(locationRoot) + + return { + variant: isRouteActive ? 'contained' : 'text', + color: isRouteActive ? 'primary' : 'secondary', + } + }, [location.pathname, to]) + + return ( + + + {children} + + + ) +} + +const AppNavbar = () => { + const navbarItems = useMemo( + () => [ + { route: Routes.Profiles, iconComponent: }, + { route: Routes.Orgs, iconComponent: }, + ], + [], + ) + + return ( + + + + {config.APP_NAME} + + + + + + {navbarItems.map(({ route, iconComponent }, idx) => ( + + {iconComponent} + + ))} + + + ) +} + +export default AppNavbar diff --git a/src/common/PageListFilters.tsx b/src/common/PageListFilters.tsx new file mode 100644 index 00000000..656f47da --- /dev/null +++ b/src/common/PageListFilters.tsx @@ -0,0 +1,41 @@ +import { InputAdornment, Stack, StackProps } from '@mui/material' +import debounce from 'lodash/debounce' +import type { ReactNode } from 'react' +import { useTranslation } from 'react-i18next' + +import { UiIcon, UiNavTabs, UiTextField } from '@/ui' + +interface Props extends StackProps { + tabs?: { + label: string + route: string + }[] + onSearchInput?: (value: string) => void + actionBar?: ReactNode +} + +export default function PageListFilters({ tabs, onSearchInput, actionBar, ...rest }: Props) { + const { t } = useTranslation() + + const handleSearchInput = debounce((value: string) => onSearchInput?.(value), 500) + + return ( + + {tabs && } + + + + + ), + }} + placeholder={t('page-list-filters.search-input-placeholder')} + onChange={e => handleSearchInput(e.target.value)} + /> + + {actionBar} + + ) +} diff --git a/src/common/PageTitles.tsx b/src/common/PageTitles.tsx new file mode 100644 index 00000000..8b0b349f --- /dev/null +++ b/src/common/PageTitles.tsx @@ -0,0 +1,23 @@ +import { Box, BoxProps, Divider, Typography } from '@mui/material' + +interface Props extends BoxProps { + title: string + subtitle?: string +} + +export default function PageTitles({ title, subtitle, ...rest }: Props) { + return ( + + + {title} + + {subtitle} + + ({ + mt: theme.spacing(6), + })} + /> + + ) +} diff --git a/src/common/index.ts b/src/common/index.ts new file mode 100644 index 00000000..03a40478 --- /dev/null +++ b/src/common/index.ts @@ -0,0 +1,3 @@ +export { default as AppNavbar } from './AppNavbar' +export { default as PageListFilters } from './PageListFilters' +export { default as PageTitles } from './PageTitles' diff --git a/src/components/AppNavbar.tsx b/src/components/AppNavbar.tsx deleted file mode 100644 index 8b2d5ea8..00000000 --- a/src/components/AppNavbar.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Divider, Stack } from '@mui/material' -import { Link } from 'react-router-dom' - -import { config } from '@/config' -import { Icons, Routes } from '@/enums' -import { UiButton, UiIcon } from '@/ui' - -const AppNavbar = () => { - return ( - - - - {config.APP_NAME} - - - - - - - - - - - - ) -} - -export default AppNavbar diff --git a/src/config.ts b/src/config.ts index 4758ee60..ebe3e35a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,5 @@ import FALLBACK_SUPPORTED_CHAINS from '@/assets/fallback-supported-chains.json' -import { Config, SupportedChainsDetails } from '@/types' +import { SupportedChains, SupportedChainsDetails } from '@/types' import packageJson from '../package.json' @@ -8,9 +8,21 @@ const SUPPORTED_CHAINS_DETAILS: SupportedChainsDetails = { ...JSON.parse(import.meta.env.VITE_SUPPORTED_CHAINS_DETAILS || '{}'), } +export type Config = { + APP_NAME: string + API_URL: string + BUILD_VERSION: string + SUPPORTED_CHAINS_DETAILS: SupportedChainsDetails + DEFAULT_CHAIN: SupportedChains + ROBOTORNOT_LINK: string + CHROME_METAMASK_ADDON_LINK: string + FIREFOX_METAMASK_ADDON_LINK: string + OPERA_METAMASK_ADDON_LINK: string +} + export const config: Config = { - MODE: import.meta.env.VITE_MODE, APP_NAME: import.meta.env.VITE_APP_NAME, + API_URL: import.meta.env.VITE_API_URL, BUILD_VERSION: packageJson.version || import.meta.env.VITE_APP_BUILD_VERSION, SUPPORTED_CHAINS_DETAILS, DEFAULT_CHAIN: import.meta.env.VITE_DEFAULT_CHAIN, diff --git a/src/contexts/toasts-manager/index.tsx b/src/contexts/toasts-manager/index.tsx index c2e1844c..f8ffd643 100644 --- a/src/contexts/toasts-manager/index.tsx +++ b/src/contexts/toasts-manager/index.tsx @@ -73,36 +73,37 @@ function ToastsManagerController({ children }: { children: ReactNode }) { [defaultTitles, defaultMessages, defaultIcons, enqueueSnackbar], ) - useEffect(() => { - const showSuccessToast = (payload?: unknown) => - showToast(BusEvents.success, payload as ToastPayload) - const showWarningToast = (payload?: unknown) => - showToast(BusEvents.warning, payload as ToastPayload) - const showErrorToast = (payload?: unknown) => - showToast(BusEvents.error, payload as ToastPayload) - const showInfoToast = (payload?: unknown) => showToast(BusEvents.info, payload as ToastPayload) - - let mountingInit = async () => { - bus.on(BusEvents.success, showSuccessToast) - bus.on(BusEvents.warning, showWarningToast) - bus.on(BusEvents.error, showErrorToast) - bus.on(BusEvents.info, showInfoToast) - } + const showSuccessToast = useCallback( + (payload?: unknown) => showToast(BusEvents.success, payload as ToastPayload), + [showToast], + ) - mountingInit() + const showWarningToast = useCallback( + (payload?: unknown) => showToast(BusEvents.warning, payload as ToastPayload), + [showToast], + ) + const showErrorToast = useCallback( + (payload?: unknown) => showToast(BusEvents.error, payload as ToastPayload), + [showToast], + ) + const showInfoToast = useCallback( + (payload?: unknown) => showToast(BusEvents.info, payload as ToastPayload), + [showToast], + ) + + useEffect(() => { + bus.on(BusEvents.success, showSuccessToast) + bus.on(BusEvents.warning, showWarningToast) + bus.on(BusEvents.error, showErrorToast) + bus.on(BusEvents.info, showInfoToast) return () => { bus.off(BusEvents.success, showSuccessToast) bus.off(BusEvents.warning, showWarningToast) bus.off(BusEvents.error, showErrorToast) bus.off(BusEvents.info, showInfoToast) - - mountingInit = async () => { - /* empty */ - } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [showErrorToast, showInfoToast, showSuccessToast, showWarningToast]) return children } diff --git a/src/enums/icons.ts b/src/enums/icons.ts index fc71ee54..5f56be96 100644 --- a/src/enums/icons.ts +++ b/src/enums/icons.ts @@ -1,10 +1,13 @@ import { default as AccountCircleIcon } from '@mui/icons-material/AccountCircle' +import { default as Add } from '@mui/icons-material/Add' import { default as CheckIcon } from '@mui/icons-material/Check' import { default as DeleteIcon } from '@mui/icons-material/Delete' import { default as ErrorOutlineIcon } from '@mui/icons-material/ErrorOutline' import { default as InfoIcon } from '@mui/icons-material/Info' import { default as KeyboardArrowDownOutlinedIcon } from '@mui/icons-material/KeyboardArrowDownOutlined' +import { default as Search } from '@mui/icons-material/Search' import { default as WarningAmberIcon } from '@mui/icons-material/WarningAmber' +import { default as Work } from '@mui/icons-material/Work' export enum Icons { Metamask = 'metamask', @@ -19,4 +22,7 @@ export const ICON_COMPONENTS = { warningAmber: WarningAmberIcon, check: CheckIcon, keyboardArrowDownOutlined: KeyboardArrowDownOutlinedIcon, + work: Work, + search: Search, + add: Add, } diff --git a/src/enums/routes.ts b/src/enums/routes.ts index 35429197..aa3a887a 100644 --- a/src/enums/routes.ts +++ b/src/enums/routes.ts @@ -2,5 +2,8 @@ export enum Routes { Root = '/', UiKit = '/ui-kit', Profiles = '/profiles', + // FIXME: how to avoid * in the path? + Orgs = '/organisations/*', + OrgNew = '/organisations/new', SignIn = '/sign-in', } diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index 4af2fb1e..2dbc880d 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -1,7 +1,7 @@ import { Stack } from '@mui/material' import { Outlet } from 'react-router-dom' -import AppNavbar from '@/components/AppNavbar' +import { AppNavbar } from '@/common' const MainLayout = () => { return ( diff --git a/src/locales/resources/en.json b/src/locales/resources/en.json index 28437265..33280f18 100644 --- a/src/locales/resources/en.json +++ b/src/locales/resources/en.json @@ -45,5 +45,12 @@ "primitive-array-title": "Primitive array", "object-array-title": "Object array", "object-and-array-combine-title": "Complex array & Object" + }, + "page-list-filters": { + "search-input-placeholder": "Search" + }, + "org-list": { + "title": "Organizations", + "subtitle": "Manage your identity credentials and Soulbound Tokens (SBTs) easily from this dashboard" } } diff --git a/src/pages/OrgNew/index.tsx b/src/pages/OrgNew/index.tsx new file mode 100644 index 00000000..92b46778 --- /dev/null +++ b/src/pages/OrgNew/index.tsx @@ -0,0 +1,5 @@ +import { Stack } from '@mui/material' + +export default function OrgNew() { + return Organization create page +} diff --git a/src/pages/Orgs/components/List.tsx b/src/pages/Orgs/components/List.tsx new file mode 100644 index 00000000..a40e6a3d --- /dev/null +++ b/src/pages/Orgs/components/List.tsx @@ -0,0 +1,41 @@ +import { Stack, StackProps } from '@mui/material' +import { useCallback } from 'react' + +import { loadOrgs, Organization, type OrgsRequestFiltersMap } from '@/api' +import { useLoading } from '@/hooks' + +interface Props extends StackProps { + filter: OrgsRequestFiltersMap +} + +export default function List({ filter, ...rest }: Props) { + // TODO: add pagination + const loadList = useCallback(async () => { + const { data } = await loadOrgs({ filter }) + + return data + }, [filter]) + + const { + data: orgList, + isLoading, + isLoadingError, + isEmpty, + } = useLoading([], loadList, { + loadOnMount: true, + }) + + return ( + + {isLoading ? ( + <> + ) : isLoadingError ? ( + <> + ) : isEmpty ? ( + <> + ) : ( + orgList.map(org =>
{org.domain}
) + )} +
+ ) +} diff --git a/src/pages/Orgs/components/index.ts b/src/pages/Orgs/components/index.ts new file mode 100644 index 00000000..f12006c1 --- /dev/null +++ b/src/pages/Orgs/components/index.ts @@ -0,0 +1 @@ +export { default as List } from './List' diff --git a/src/pages/Orgs/index.tsx b/src/pages/Orgs/index.tsx new file mode 100644 index 00000000..70c16807 --- /dev/null +++ b/src/pages/Orgs/index.tsx @@ -0,0 +1,106 @@ +import { Stack } from '@mui/material' +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Navigate, NavLink, Route, Routes } from 'react-router-dom' + +import { loadOrgsAmount, OrgsRequestFilters, OrgsRequestFiltersMap } from '@/api' +import { PageListFilters, PageTitles } from '@/common' +import { Routes as RoutesPaths } from '@/enums' +import { useLoading } from '@/hooks' +import { UiButton, UiIcon, UiSwitch } from '@/ui' + +import { List } from './components' + +enum OrgsRoutes { + All = 'all', + My = 'my', +} + +export default function Orgs() { + const { t } = useTranslation() + + const [filter, setFilter] = useState({}) + + const init = useCallback(async () => { + const { data } = await loadOrgsAmount() + + return data + }, []) + + const { data: orgsAmount } = useLoading(undefined, init, { + loadOnMount: true, + }) + + return ( + + + + setFilter(prev => ({ + ...prev, + + // FIXME: remove this and add searching orgs by name in backend + [OrgsRequestFilters.UserDid]: value, + })) + } + actionBar={ + + + + + } + > + New Organization + + + + } + /> + + + + + } + /> + + } + /> + + } /> + + + + ) +} diff --git a/src/pages/UiKit/index.tsx b/src/pages/UiKit/index.tsx index a2cadede..4aa1dbe4 100644 --- a/src/pages/UiKit/index.tsx +++ b/src/pages/UiKit/index.tsx @@ -1,67 +1,20 @@ -import { Box, Stack, Tab, Tabs } from '@mui/material' -import { ReactNode, SyntheticEvent, useState } from 'react' +import { Stack } from '@mui/material' import UiKitButtons from '@/pages/UiKit/UiKitButtons' import UiKitFields from '@/pages/UiKit/UiKitFields' import UiKitOther from '@/pages/UiKit/UiKitOther' - -const a11yProps = (index: number) => { - return { - id: `simple-tab-${index}`, - 'aria-controls': `simple-tabpanel-${index}`, - } -} - -function CustomTabPanel({ - children, - value, - index, - ...rest -}: { - children?: ReactNode - index: number - value: number -}) { - return ( - - ) -} +import { UiTabs } from '@/ui' export default function UiKit() { - const [currentTab, setCurrentTab] = useState(0) - - const handleChange = (event: SyntheticEvent, newValue: number) => { - setCurrentTab(newValue) - } - return ( - - - - - - - - - - - - - - - - + }, + { label: 'FIELDS', content: }, + { label: 'OTHER', content: }, + ]} + /> ) } diff --git a/src/routes.tsx b/src/routes.tsx index fdb74809..3e1568a5 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -23,8 +23,11 @@ import MainLayout from './layouts/MainLayout' export const AppRoutes = () => { const SignIn = lazy(() => import('@/pages/SignIn')) + const Orgs = lazy(() => import('@/pages/Orgs')) + const OrgNew = lazy(() => import('@/pages/OrgNew')) + // TODO: Replace with real auth check - const isAuthorized = false + const isAuthorized = true const signInGuard = () => (isAuthorized ? redirect(Routes.Root) : null) const authProtectedGuard = ({ request }: LoaderFunctionArgs) => { @@ -66,6 +69,16 @@ export const AppRoutes = () => { loader: authProtectedGuard, element: , }, + { + path: Routes.Orgs, + loader: authProtectedGuard, + element: , + }, + { + path: Routes.OrgNew, + loader: authProtectedGuard, + element: , + }, ], }, { diff --git a/src/theme/typography.ts b/src/theme/typography.ts index aaac0608..c26cffde 100644 --- a/src/theme/typography.ts +++ b/src/theme/typography.ts @@ -51,10 +51,10 @@ export const typographyTheme: TypographyOptions = { }, h6: { fontFamily: TYPOGRAPHY.txtFontFamily, - fontWeight: TYPOGRAPHY.txtFontWeightH5, - fontSize: TYPOGRAPHY.txtFontSizeH5, - lineHeight: TYPOGRAPHY.txtFontLineHeightH5, - letterSpacing: TYPOGRAPHY.txtFontLetterSpacingH5, + fontWeight: TYPOGRAPHY.txtFontWeightH6, + fontSize: TYPOGRAPHY.txtFontSizeH6, + lineHeight: TYPOGRAPHY.txtFontLineHeightH6, + letterSpacing: TYPOGRAPHY.txtFontLetterSpacingH6, }, subtitle1: { fontFamily: TYPOGRAPHY.txtFontFamily, diff --git a/src/types/config.ts b/src/types/config.ts deleted file mode 100644 index 5a3a6a96..00000000 --- a/src/types/config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SupportedChains, SupportedChainsDetails } from './chains' - -export type BuildMode = 'production' | 'development' - -export type Config = { - MODE: BuildMode - APP_NAME: string - BUILD_VERSION: string - SUPPORTED_CHAINS_DETAILS: SupportedChainsDetails - DEFAULT_CHAIN: SupportedChains - ROBOTORNOT_LINK: string - CHROME_METAMASK_ADDON_LINK: string - FIREFOX_METAMASK_ADDON_LINK: string - OPERA_METAMASK_ADDON_LINK: string -} diff --git a/src/types/index.ts b/src/types/index.ts index 37a8aea3..09dcb3e2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,5 @@ export * from './base' export * from './bus' export * from './chains' -export * from './config' export * from './theme' export * from './web3' diff --git a/src/ui/UiNavTabs.tsx b/src/ui/UiNavTabs.tsx new file mode 100644 index 00000000..56afc50e --- /dev/null +++ b/src/ui/UiNavTabs.tsx @@ -0,0 +1,61 @@ +import { ButtonProps, Stack, StackProps } from '@mui/material' +import { useMemo } from 'react' +import { NavLink, NavLinkProps, useLocation } from 'react-router-dom' + +import UiButton from '@/ui/UiButton' + +interface Props extends StackProps { + tabs: { + label: string + route: string + }[] +} + +function NavTab({ to, label, ...rest }: { label: string } & NavLinkProps) { + const location = useLocation() + + const getLinkProps = useMemo((): Partial => { + const isRouteActive = location.pathname.includes(to as string) + + return { + variant: isRouteActive ? 'contained' : 'text', + color: isRouteActive ? 'primary' : 'secondary', + } + }, [location.pathname, to]) + + return ( + + ({ + background: theme.palette.background.default, + px: 7, + py: 2, + })} + {...getLinkProps} + > + {label} + + + ) +} + +export default function UiNavTabs({ tabs, ...rest }: Props) { + return ( + ({ + background: theme.palette.text.secondary, + borderRadius: 25, + overflow: 'hidden', + })} + padding={1} + > + {tabs.map(({ route, label }, idx) => ( + + ))} + + ) +} diff --git a/src/ui/UiSwitch.tsx b/src/ui/UiSwitch.tsx index 611c492e..4be77cac 100644 --- a/src/ui/UiSwitch.tsx +++ b/src/ui/UiSwitch.tsx @@ -6,7 +6,7 @@ interface Props extends SwitchProps { export default function UiSwitch({ label, ...rest }: Props) { return label ? ( - } label='Label' /> + } label={label} /> ) : ( ) diff --git a/src/ui/UiTabs.tsx b/src/ui/UiTabs.tsx new file mode 100644 index 00000000..44908e63 --- /dev/null +++ b/src/ui/UiTabs.tsx @@ -0,0 +1,68 @@ +import { Box, Stack, StackProps, Tab, Tabs } from '@mui/material' +import { ReactNode, SyntheticEvent, useCallback, useState } from 'react' + +interface Props extends StackProps { + tabs: { + label: string + content: ReactNode + }[] + ariaLabel?: string +} + +const a11yProps = (index: number) => { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + } +} + +function CustomTabPanel({ + children, + value, + index, + ...rest +}: { + children?: ReactNode + index: number + value: number +}) { + return ( + + ) +} + +export default function UiTabs({ tabs, ariaLabel, ...rest }: Props) { + const [currentTab, setCurrentTab] = useState(0) + + const handleChange = useCallback((event: SyntheticEvent, newValue: number) => { + setCurrentTab(newValue) + }, []) + + return ( + + + + {tabs.map(({ label }, idx) => ( + + ))} + + + + {tabs.map(({ content }, idx) => ( + + {content} + + ))} + + ) +} diff --git a/src/ui/index.ts b/src/ui/index.ts index 4ac19d64..7c34d83a 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -5,7 +5,9 @@ export { default as UiDrawer } from './UiDrawer' export { default as UiIcon } from './UiIcon' export { default as UiIconButton } from './UiIconButton' export { default as UiModal } from './UiModal' +export { default as UiNavTabs } from './UiNavTabs' export { default as UiRadioGroup } from './UiRadioGroup' export { default as UiSelect } from './UiSelect' export { default as UiSwitch } from './UiSwitch' +export { default as UiTabs } from './UiTabs' export { default as UiTextField } from './UiTextField' diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 18ee0f77..48274035 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -4,6 +4,7 @@ import { BuildMode, SupportedChains } from '@/types' interface ImportMetaEnv { VITE_MODE: BuildMode + VITE_API_URL: string VITE_PORT: string VITE_APP_NAME: string VITE_APP_BUILD_VERSION: string diff --git a/yarn.lock b/yarn.lock index 3e122036..d9eda052 100644 --- a/yarn.lock +++ b/yarn.lock @@ -124,6 +124,25 @@ __metadata: languageName: node linkType: hard +"@distributedlab/fetcher@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@distributedlab/fetcher@npm:1.0.0-rc.9" + dependencies: + uuid: "npm:^9.0.0" + checksum: 7f1da4d3348c76881e7fa954b58e3b5bc55ecf7d04f9916451af0a83ee9da9a694c726b66e549470592bd13045f068aef10a97b920eeb6c6f137167704bea06a + languageName: node + linkType: hard + +"@distributedlab/jac@npm:^1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@distributedlab/jac@npm:1.0.0-rc.9" + dependencies: + "@distributedlab/fetcher": "npm:1.0.0-rc.9" + lodash: "npm:^4.17.21" + checksum: 6da9c42004e66be4910d10eca013a9d67c67569b903b3903da665c2adbb3281f4d36e7527143899f38898ad0e30b991d6ee36e42650a8f0170e6a8c87baffd20 + languageName: node + linkType: hard + "@distributedlab/tools@npm:1.0.0-rc.9, @distributedlab/tools@npm:^1.0.0-rc.9": version: 1.0.0-rc.9 resolution: "@distributedlab/tools@npm:1.0.0-rc.9" @@ -4504,6 +4523,7 @@ __metadata: resolution: "dashboard-rarime@workspace:." dependencies: "@comparaonline/stylelint-config-scss-modules": "npm:^1.1.0" + "@distributedlab/jac": "npm:^1.0.0-rc.9" "@distributedlab/tools": "npm:^1.0.0-rc.9" "@distributedlab/w3p": "npm:^1.0.0-rc.9" "@emotion/react": "npm:^11.11.1" @@ -12822,7 +12842,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.1": +"uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: