chore(layouts): Consolidation and comprehensive typing (#6)
* Consolidate layout pattern * Duplicates cleared * Tsdoc function docs * Excess files * Full types transition * Thorough tsdoc of existing comoponents
This commit is contained in:
parent
a1be980976
commit
52512beaa2
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,3 +10,4 @@ packages/frontend/dist/
|
||||
|
||||
# ignore all .DS_Store files
|
||||
**/.DS_Store
|
||||
.vscode
|
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@ -1,9 +0,0 @@
|
||||
{
|
||||
// IntelliSense for taiwind variants
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
"tv\\('([^)]*)\\')",
|
||||
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
|
||||
],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
@ -1,22 +1,22 @@
|
||||
import debug from 'debug';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { ApolloServer } from 'apollo-server-express';
|
||||
import { createServer } from 'http';
|
||||
import {
|
||||
ApolloServerPluginDrainHttpServer,
|
||||
ApolloServerPluginLandingPageLocalDefault,
|
||||
AuthenticationError,
|
||||
} from 'apollo-server-core';
|
||||
import { ApolloServer } from 'apollo-server-express';
|
||||
import cors from 'cors';
|
||||
import debug from 'debug';
|
||||
import express from 'express';
|
||||
import session from 'express-session';
|
||||
import { createServer } from 'http';
|
||||
|
||||
import { TypeSource } from '@graphql-tools/utils';
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||
import { TypeSource } from '@graphql-tools/utils';
|
||||
|
||||
import { ServerConfig } from './config';
|
||||
import { DEFAULT_GQL_PATH } from './constants';
|
||||
import githubRouter from './routes/github';
|
||||
import authRouter from './routes/auth';
|
||||
import githubRouter from './routes/github';
|
||||
import stagingRouter from './routes/staging';
|
||||
import { Service } from './service';
|
||||
|
||||
@ -101,7 +101,7 @@ export const createAndStartServer = async (
|
||||
}
|
||||
|
||||
app.use(
|
||||
session(sessionOptions)
|
||||
session(sessionOptions) as unknown as express.RequestHandler
|
||||
);
|
||||
|
||||
server.applyMiddleware({
|
||||
@ -116,9 +116,9 @@ export const createAndStartServer = async (
|
||||
app.use(express.json());
|
||||
|
||||
app.set('service', service);
|
||||
app.use('/auth', authRouter);
|
||||
app.use('/api/github', githubRouter);
|
||||
app.use('/staging', stagingRouter);
|
||||
app.use('/auth', authRouter as express.RequestHandler);
|
||||
app.use('/api/github', githubRouter as express.RequestHandler);
|
||||
app.use('/staging', stagingRouter as express.RequestHandler);
|
||||
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
console.error(err);
|
||||
|
@ -1,25 +0,0 @@
|
||||
export const tabPageConfig: TabPageNavigationConfig = {
|
||||
defaultTab: 'tab1',
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab1',
|
||||
label: 'Overview',
|
||||
content: <div>This is the overview content</div>,
|
||||
},
|
||||
{
|
||||
id: 'tab2',
|
||||
label: 'Details',
|
||||
content: <div>This is the details content</div>,
|
||||
},
|
||||
{
|
||||
id: 'tab3',
|
||||
label: 'Settings',
|
||||
content: <div>This is the settings content</div>,
|
||||
},
|
||||
{
|
||||
id: 'tab4',
|
||||
label: 'History',
|
||||
content: <div>This is the history content</div>,
|
||||
},
|
||||
],
|
||||
};
|
@ -115,7 +115,7 @@
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"@types/luxon": "^3.3.7",
|
||||
"@types/node": "^16.18.68",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/uuid": "^9.0.8",
|
||||
|
@ -1,37 +1,32 @@
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import { useEffect } from 'react';
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
|
||||
import { BASE_URL } from 'utils/constants';
|
||||
import ProjectSearchLayout from './layouts/ProjectSearch';
|
||||
import { DashboardLayout } from './layouts/DashboardLayout';
|
||||
import Index from './pages';
|
||||
import AuthPage from './pages/AuthPage';
|
||||
import BuyPrepaidService from './pages/BuyPrepaidService';
|
||||
import Projects from './pages/org-slug';
|
||||
import { projectsRoutesWithoutSearch } from './pages/org-slug/projects/project-routes';
|
||||
import Settings from './pages/org-slug/Settings';
|
||||
import { DashboardLayout } from './pages/org-slug/layout';
|
||||
import {
|
||||
projectsRoutesWithoutSearch,
|
||||
projectsRoutesWithSearch,
|
||||
} from './pages/org-slug/projects/routes';
|
||||
import { BASE_URL } from './utils/constants';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: ':orgSlug',
|
||||
element: <DashboardLayout />,
|
||||
children: [
|
||||
{
|
||||
element: <ProjectSearchLayout />,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <Projects />,
|
||||
},
|
||||
{
|
||||
path: 'projects',
|
||||
children: projectsRoutesWithSearch,
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// element: <ProjectSearchLayout />,
|
||||
// children: [
|
||||
// {
|
||||
// path: '',
|
||||
// element: <Projects />,
|
||||
// },
|
||||
// {
|
||||
// path: 'projects',
|
||||
// children: projectsRoutesWithSearch,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
path: 'settings',
|
||||
element: <Settings />,
|
||||
@ -56,6 +51,11 @@ const router = createBrowserRouter([
|
||||
},
|
||||
]);
|
||||
|
||||
/**
|
||||
* Main application component.
|
||||
* Sets up routing, authentication, and theme provider.
|
||||
* @returns {JSX.Element} The rendered application.
|
||||
*/
|
||||
function App() {
|
||||
// Hacky way of checking session
|
||||
// TODO: Handle redirect backs
|
||||
@ -66,7 +66,6 @@ function App() {
|
||||
const path = window.location.pathname;
|
||||
if (res.status !== 200) {
|
||||
localStorage.clear();
|
||||
|
||||
if (path !== '/login') {
|
||||
window.location.pathname = '/login';
|
||||
}
|
||||
@ -78,7 +77,11 @@ function App() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<RouterProvider router={router} fallbackElement={<div>Loading...</div>} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {
|
||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO,
|
||||
VITE_GITHUB_PWA_TEMPLATE_REPO,
|
||||
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO,
|
||||
} from 'utils/constants';
|
||||
VITE_GITHUB_PWA_TEMPLATE_REPO,
|
||||
} from '@/utils/constants';
|
||||
|
||||
export default [
|
||||
{
|
||||
|
@ -1,219 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = React.PropsWithChildren<{
|
||||
className?: string;
|
||||
snowZIndex?: number;
|
||||
}>;
|
||||
|
||||
export const CloudyFlow = ({ className, children, snowZIndex }: Props) => {
|
||||
return (
|
||||
<div className={`bg-sky-100 relative ${className || ''}`}>
|
||||
{children}
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={{ zIndex: snowZIndex || 0 }}
|
||||
>
|
||||
<div className="w-[3.72px] h-[3.72px] left-[587px] top-[147px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.72px] h-[4.72px] left-[742px] top-[336px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.49px] h-[3.49px] left-[36px] top-[68px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.25px] h-[3.25px] left-[55px] top-[114px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.60px] h-[5.60px] left-[1334px] top-[63px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.53px] h-[3.53px] left-[988px] top-[108px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.65px] h-[2.65px] left-[1380px] top-[16px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.60px] h-[3.60px] left-[1284px] top-[95px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-0.5 h-0.5 left-[1191px] top-[376px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.83px] h-[2.83px] left-[1182px] top-[257px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.41px] h-[2.41px] left-[627px] top-[26px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.71px] h-[5.71px] left-[30px] top-[33px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.09px] h-[4.09px] left-[425px] top-[386px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.38px] h-[3.38px] left-[394px] top-[29px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.70px] h-[4.70px] left-[817px] top-[113px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-1.5 h-1.5 left-[1194px] top-[332px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.89px] h-[4.89px] left-[811px] top-[76px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.25px] h-[4.25px] left-[458px] top-[366px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.82px] h-[4.82px] left-[936px] top-[46px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.74px] h-[3.74px] left-[64px] top-[132px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-1 h-1 left-[763px] top-[10px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.67px] h-[3.67px] left-[861px] top-[106px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.62px] h-[3.62px] left-[710px] top-[278px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.45px] h-[3.45px] left-[1069px] top-[329px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.92px] h-[2.92px] left-[1286px] top-[299px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.84px] h-[4.84px] left-[219px] top-[269px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.39px] h-[2.39px] left-[817px] top-[121px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.83px] h-[5.83px] left-[168px] top-[320px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.94px] h-[5.94px] left-[419px] top-[244px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.67px] h-[4.67px] left-[604px] top-[309px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.87px] h-[5.87px] left-[1098px] top-[379px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.85px] h-[5.85px] left-[644px] top-[352px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.19px] h-[4.19px] left-[1361px] top-[349px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.84px] h-[2.84px] left-[1299px] top-[194px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.51px] h-[4.51px] left-[468px] top-[319px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.73px] h-[2.73px] left-[1084px] top-[86px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.43px] h-[3.43px] left-[1271px] top-[28px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.25px] h-[2.25px] left-[106px] top-[197px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.82px] h-[2.82px] left-[122px] top-[173px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.89px] h-[2.89px] left-[343px] top-[345px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.82px] h-[2.82px] left-[433px] top-[40px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[904px] top-[350px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.42px] h-[4.42px] left-[1066px] top-[349px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.67px] h-[4.67px] left-[904px] top-[317px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.54px] h-[5.54px] left-[501px] top-[336px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[1149px] top-[206px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.55px] h-[3.55px] left-[235px] top-[362px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.60px] h-[2.60px] left-[1246px] top-[1px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.94px] h-[2.94px] left-[788px] top-[6px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.19px] h-[4.19px] left-[527px] top-[365px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.13px] h-[4.13px] left-[201px] top-[53px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.94px] h-[2.94px] left-[765px] top-[13px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[1254px] top-[30px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.85px] h-[3.85px] left-[107px] top-[316px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.72px] h-[5.72px] left-[1305px] top-[8px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.46px] h-[5.46px] left-[102px] top-[316px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.77px] h-[3.77px] left-[1322px] top-[334px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.84px] h-[4.84px] left-[1370px] top-[317px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.55px] h-[5.55px] left-[945px] top-[258px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.24px] h-[2.24px] left-[266px] top-[362px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.89px] h-[2.89px] left-[987px] top-[156px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.46px] h-[3.46px] left-[10px] top-[168px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.67px] h-[5.67px] left-[441px] top-[291px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.07px] h-[4.07px] left-[962px] top-[364px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.57px] h-[5.57px] left-[599px] top-[293px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.41px] h-[4.41px] left-[358px] top-[163px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.31px] h-[2.31px] left-[670px] top-[182px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.60px] h-[2.60px] left-[621px] top-[257px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.16px] h-[2.16px] left-[48px] top-[322px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.91px] h-[5.91px] left-[491px] top-[5px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.50px] h-[5.50px] left-[1139px] top-[274px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.74px] h-[3.74px] left-[24px] top-[177px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.57px] h-[5.57px] left-[1166px] top-[316px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5px] h-[5px] left-[445px] top-[326px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.01px] h-[3.01px] left-[438px] top-[252px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.14px] h-[4.14px] left-[554px] top-[131px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.30px] h-[5.30px] left-[1010px] top-[116px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.53px] h-[5.53px] left-[437px] top-[367px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.87px] h-[5.87px] left-[948px] top-[27px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.87px] h-[2.87px] left-[826px] top-[20px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.89px] h-[3.89px] left-[1222px] top-[112px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.77px] h-[3.77px] left-[796px] top-[395px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.09px] h-[2.09px] left-[272px] top-[103px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.12px] h-[4.12px] left-[76px] top-[2px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.51px] h-[3.51px] left-[226px] top-[276px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.03px] h-[3.03px] left-[723px] top-[197px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.14px] h-[2.14px] left-[1259px] top-[17px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.28px] h-[3.28px] left-[1244px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.45px] h-[4.45px] left-[118px] top-[128px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.15px] h-[4.15px] left-[490px] top-[204px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.93px] h-[4.93px] left-[552px] top-[38px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.56px] h-[5.56px] left-[115px] top-[303px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.35px] h-[2.35px] left-[509px] top-[278px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.24px] h-[5.24px] left-[804px] top-[389px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.44px] h-[2.44px] left-[1013px] top-[50px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.69px] h-[3.69px] left-[1183px] top-[95px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.83px] h-[2.83px] left-[278px] top-[181px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.22px] h-[3.22px] left-[1316px] top-[282px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.55px] h-[3.55px] left-[736px] top-[119px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.29px] h-[2.29px] left-[483px] top-[319px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.14px] h-[2.14px] left-[1135px] top-[19px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.64px] h-[3.64px] left-[39px] top-[126px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.30px] h-[5.30px] left-[237px] top-[369px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.57px] h-[5.57px] left-[1156px] top-[126px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.78px] h-[2.78px] left-[1295px] top-[74px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-0.5 h-0.5 left-[76px] top-[227px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.61px] h-[3.61px] left-[108px] top-[89px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.37px] h-[5.37px] left-[191px] top-[167px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.18px] h-[4.18px] left-[164px] top-[117px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.15px] h-[5.15px] left-[533px] top-[261px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-1.5 h-1.5 left-[327px] top-[157px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.74px] h-[5.74px] left-[1242px] top-[122px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.22px] h-[4.22px] left-[129px] top-[265px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.30px] h-[2.30px] left-[1305px] top-[86px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.70px] h-[2.70px] left-[1235px] top-[120px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.15px] h-[2.15px] left-[596px] top-[103px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.17px] h-[2.17px] left-[483px] top-[233px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.09px] h-[5.09px] left-[706px] top-[188px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.15px] h-[4.15px] left-[141px] top-[2px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.20px] h-[4.20px] left-[48px] top-[124px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.51px] h-[3.51px] left-[1095px] top-[201px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.21px] h-[3.21px] left-[730px] top-[185px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.61px] h-[2.61px] left-[722px] top-[319px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.28px] h-[2.28px] left-[444px] top-[26px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.49px] h-[4.49px] left-[355px] top-[212px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.69px] h-[3.69px] left-[1280px] top-[312px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.23px] h-[4.23px] left-[1114px] top-[113px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.48px] h-[3.48px] left-[729px] top-[117px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[647px] top-[276px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.16px] h-[4.16px] left-[365px] top-[116px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.35px] h-[5.35px] left-[94px] top-[194px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.84px] h-[5.84px] left-[2px] top-[84px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.43px] h-[4.43px] left-[1382px] top-[23px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.38px] h-[5.38px] left-[857px] top-[284px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.77px] h-[2.77px] left-[1228px] top-[385px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.65px] h-[4.65px] left-[165px] top-[184px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.53px] h-[5.53px] left-[568px] top-[354px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.59px] h-[3.59px] left-[1303px] top-[371px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.84px] h-[5.84px] left-[235px] top-[188px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.84px] h-[3.84px] left-[902px] top-[211px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.45px] h-[3.45px] left-[367px] top-[161px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.08px] h-[4.08px] left-[855px] top-[394px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.25px] h-[3.25px] left-[383px] top-[47px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.39px] h-[4.39px] left-[1313px] top-[165px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.60px] h-[5.60px] left-[697px] top-[327px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.09px] h-[2.09px] left-[646px] top-[370px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.13px] h-[3.13px] left-[728px] top-[122px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.53px] h-[5.53px] left-[203px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.83px] h-[5.83px] left-[424px] top-[121px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.82px] h-[4.82px] left-[1358px] top-[176px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.18px] h-[3.18px] left-[1212px] top-[24px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.23px] h-[5.23px] left-[260px] top-[217px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.29px] h-[5.29px] left-[1204px] top-[367px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.47px] h-[3.47px] left-[1163px] top-[159px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.77px] h-[5.77px] left-[1257px] top-[115px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.31px] h-[5.31px] left-[222px] top-[356px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.43px] h-[5.43px] left-[1141px] top-[349px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.62px] h-[5.62px] left-[683px] top-[81px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.91px] h-[3.91px] left-[269px] top-[3px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.51px] h-[3.51px] left-[305px] top-[310px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.41px] h-[5.41px] left-[530px] top-[94px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.64px] h-[4.64px] left-[730px] top-[301px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.59px] h-[3.59px] left-[716px] top-[14px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.77px] h-[4.77px] left-[544px] top-[13px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.29px] h-[2.29px] left-[357px] top-[281px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.42px] h-[2.42px] left-[1346px] top-[112px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.42px] h-[3.42px] left-[671px] top-[150px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.40px] h-[4.40px] left-[1324px] top-[268px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.21px] h-[5.21px] left-[1028px] top-[376px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.27px] h-[4.27px] left-[499px] top-[50px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.35px] h-[4.35px] left-[543px] top-[359px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.25px] h-[5.25px] left-[1245px] top-[296px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.52px] h-[5.52px] left-[360px] top-[98px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.46px] h-[4.46px] left-[741px] top-[358px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.90px] h-[3.90px] left-[1262px] top-[184px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.75px] h-[5.75px] left-[552px] top-[335px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.95px] h-[4.95px] left-[120px] top-[178px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.28px] h-[3.28px] left-[1337px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.43px] h-[2.43px] left-[233px] top-[310px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-1 h-1 left-[218px] top-[322px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.68px] h-[3.68px] left-[984px] top-[8px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.44px] h-[2.44px] left-[832px] top-[55px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.93px] h-[3.93px] left-[1105px] top-[209px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.08px] h-[4.08px] left-[957px] top-[23px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.33px] h-[2.33px] left-[1066px] top-[390px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.25px] h-[3.25px] left-[737px] top-[118px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.18px] h-[5.18px] left-[202px] top-[19px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.05px] h-[5.05px] left-[466px] top-[17px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.85px] h-[3.85px] left-[144px] top-[153px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.35px] h-[5.35px] left-[233px] top-[330px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-1 h-1 left-[730px] top-[179px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.46px] h-[4.46px] left-[1156px] top-[342px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.22px] h-[5.22px] left-[1275px] top-[204px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.50px] h-[5.50px] left-[38px] top-[343px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.14px] h-[5.14px] left-[867px] top-[113px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.19px] h-[2.19px] left-[1277px] top-[314px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.74px] h-[3.74px] left-[1136px] top-[197px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.37px] h-[5.37px] left-[34px] top-[226px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.93px] h-[5.93px] left-[727px] top-[272px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.29px] h-[5.29px] left-[277px] top-[43px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,172 +0,0 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
DayPicker,
|
||||
SelectSingleEventHandler,
|
||||
DateRange,
|
||||
} from 'react-day-picker';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverHandler,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import HorizontalLine from './HorizontalLine';
|
||||
|
||||
// https://www.material-tailwind.com/docs/react/plugins/date-picker#date-picker
|
||||
const DAY_PICKER_CLASS_NAMES = {
|
||||
caption: 'flex justify-center py-2 mb-4 relative items-center',
|
||||
caption_label: 'text-sm font-medium text-gray-900',
|
||||
nav: 'flex items-center',
|
||||
nav_button:
|
||||
'h-6 w-6 bg-transparent hover:bg-blue-gray-50 p-1 rounded-md transition-colors duration-300',
|
||||
nav_button_previous: 'absolute left-1.5',
|
||||
nav_button_next: 'absolute right-1.5',
|
||||
table: 'w-full border-collapse',
|
||||
head_row: 'flex font-medium text-gray-900',
|
||||
head_cell: 'm-0.5 w-9 font-normal text-sm',
|
||||
row: 'flex w-full mt-2',
|
||||
cell: 'text-gray-600 rounded-md h-9 w-9 text-center text-sm p-0 m-0.5 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-gray-900/20 [&:has([aria-selected].day-outside)]:text-white [&:has([aria-selected])]:bg-gray-900/50 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
|
||||
day: 'h-9 w-9 p-0 font-normal',
|
||||
day_range_end: 'day-range-end',
|
||||
day_selected:
|
||||
'rounded-md bg-gray-900 text-white hover:bg-gray-900 hover:text-white focus:bg-gray-900 focus:text-white',
|
||||
day_today: 'rounded-md bg-gray-200 text-gray-900',
|
||||
day_outside:
|
||||
'day-outside text-gray-500 opacity-50 aria-selected:bg-gray-500 aria-selected:text-gray-900 aria-selected:bg-opacity-10',
|
||||
day_disabled: 'text-gray-500 opacity-50',
|
||||
day_hidden: 'invisible',
|
||||
};
|
||||
|
||||
type SingleDateHandler = (value: Date) => void;
|
||||
type RangeDateHandler = (value: DateRange) => void;
|
||||
|
||||
interface SingleDatePickerProps {
|
||||
mode: 'single';
|
||||
selected?: Date;
|
||||
onSelect: SingleDateHandler;
|
||||
}
|
||||
|
||||
interface RangeDatePickerProps {
|
||||
mode: 'range';
|
||||
selected?: DateRange;
|
||||
onSelect: RangeDateHandler;
|
||||
}
|
||||
|
||||
const DatePicker = ({
|
||||
mode = 'single',
|
||||
selected,
|
||||
onSelect,
|
||||
}: SingleDatePickerProps | RangeDatePickerProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [rangeSelected, setRangeSelected] = useState<DateRange>();
|
||||
|
||||
const inputValue = useMemo(() => {
|
||||
if (mode === 'single') {
|
||||
return selected ? format(selected as Date, 'PPP') : 'Select Date';
|
||||
}
|
||||
|
||||
if (mode === 'range') {
|
||||
const selectedRange = selected as DateRange | undefined;
|
||||
return selectedRange && selectedRange.from && selectedRange.to
|
||||
? format(selectedRange.from, 'PP') +
|
||||
'-' +
|
||||
format(selectedRange.to, 'PP')
|
||||
: 'All time';
|
||||
}
|
||||
}, [selected, mode]);
|
||||
|
||||
const handleSingleSelect = useCallback<SelectSingleEventHandler>((value) => {
|
||||
if (value) {
|
||||
(onSelect as SingleDateHandler)(value);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleRangeSelect = useCallback(() => {
|
||||
if (rangeSelected?.to) {
|
||||
(onSelect as RangeDateHandler)(rangeSelected);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [rangeSelected]);
|
||||
|
||||
const components = {
|
||||
IconLeft: ({ ...props }) => (
|
||||
<i {...props} className="h-4 w-4 stroke-2">
|
||||
{'<'}
|
||||
</i>
|
||||
),
|
||||
IconRight: ({ ...props }) => (
|
||||
<i {...props} className="h-4 w-4 stroke-2">
|
||||
{'>'}
|
||||
</i>
|
||||
),
|
||||
};
|
||||
|
||||
const commonDayPickerProps = {
|
||||
components,
|
||||
className: 'border-0',
|
||||
classNames: DAY_PICKER_CLASS_NAMES,
|
||||
showOutsideDays: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="bottom"
|
||||
open={isOpen}
|
||||
handler={(value) => setIsOpen(value)}
|
||||
>
|
||||
<PopoverHandler>
|
||||
<Input onChange={() => null} value={inputValue} />
|
||||
</PopoverHandler>
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
{/* @ts-ignore */}
|
||||
<PopoverContent>
|
||||
{mode === 'single' && (
|
||||
<DayPicker
|
||||
mode="single"
|
||||
onSelect={handleSingleSelect}
|
||||
selected={selected as Date}
|
||||
{...commonDayPickerProps}
|
||||
/>
|
||||
)}
|
||||
{mode === 'range' && (
|
||||
<>
|
||||
<DayPicker
|
||||
mode="range"
|
||||
onSelect={setRangeSelected}
|
||||
selected={rangeSelected as DateRange}
|
||||
{...commonDayPickerProps}
|
||||
/>
|
||||
<HorizontalLine />
|
||||
<div className="flex justify-end">
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-full mr-2"
|
||||
variant="outlined"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-full"
|
||||
color="gray"
|
||||
onClick={() => handleRangeSelect()}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePicker;
|
@ -1,6 +1,6 @@
|
||||
import { cn } from '@/utils/classnames';
|
||||
import { Duration } from 'luxon';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
export interface FormatMilliSecondProps
|
||||
extends ComponentPropsWithoutRef<'div'> {
|
||||
|
@ -1,39 +0,0 @@
|
||||
import { NavigationWrapper } from "@/components/navigation/NavigationWrapper"
|
||||
import { ScreenWrapper } from "@/components/screen-wrapper/ScreenWrapper"
|
||||
import { Header } from "@/components/screen-header/Header"
|
||||
import { TabPageNavigation } from "@/components/tab-navigation/TabPageNavigation"
|
||||
import { ActionButton } from "@/components/screen-header/ActionButton"
|
||||
import { Circle, PlusCircle } from "lucide-react"
|
||||
import { tabPageConfig } from "@/components/tab-navigation/tabPageConfig"
|
||||
|
||||
export default function PageWithSubNav() {
|
||||
return (
|
||||
<NavigationWrapper>
|
||||
<ScreenWrapper>
|
||||
<div className="p-4 sm:p-8">
|
||||
<Header
|
||||
title="PageTitle"
|
||||
subtitle="Optional subtitle for the page"
|
||||
actions={[
|
||||
<ActionButton
|
||||
key="secondary"
|
||||
label="Second Action"
|
||||
icon={Circle}
|
||||
variant="outline"
|
||||
onClick={() => console.log("Secondary action clicked")}
|
||||
/>,
|
||||
<ActionButton
|
||||
key="primary"
|
||||
label="Action"
|
||||
icon={PlusCircle}
|
||||
onClick={() => console.log("Primary action clicked")}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<TabPageNavigation config={tabPageConfig} />
|
||||
</div>
|
||||
</ScreenWrapper>
|
||||
</NavigationWrapper>
|
||||
)
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
import type React from 'react';
|
||||
interface ScreenWrapperProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ScreenWrapper({ children }: ScreenWrapperProps) {
|
||||
return (
|
||||
<div className="min-h-[calc(100vh-4rem)] bg-background flex flex-col gap-4 bg-background xl:px-8 pt-20 pb-8 px-4">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import React, { forwardRef, RefAttributes } from 'react';
|
||||
|
||||
import { IconInput, InputProps } from '@/components/ui';
|
||||
import { Search } from 'lucide-react';
|
||||
import { Input, InputProps } from './shared/Input';
|
||||
|
||||
const SearchBar: React.ForwardRefRenderFunction<
|
||||
HTMLInputElement,
|
||||
@ -9,13 +9,13 @@ const SearchBar: React.ForwardRefRenderFunction<
|
||||
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
|
||||
return (
|
||||
<div className="relative flex w-full">
|
||||
<Input
|
||||
<IconInput
|
||||
leftIcon={<Search className="text-foreground-secondary" />}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
type="search"
|
||||
placeholder={placeholder}
|
||||
appearance="borderless"
|
||||
// appearance="borderless"
|
||||
className="w-full lg:w-[459px]"
|
||||
{...props}
|
||||
ref={ref}
|
||||
|
@ -1,11 +0,0 @@
|
||||
import {
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
Alert as UIAlert,
|
||||
} from '@/components/ui/alert';
|
||||
|
||||
export const Alert = UIAlert;
|
||||
export {
|
||||
AlertDescription,
|
||||
AlertTitle
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
import {
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
Avatar as UIAvatar,
|
||||
} from '@/components/ui/avatar';
|
||||
|
||||
export const Avatar = UIAvatar;
|
||||
export {
|
||||
AvatarFallback, AvatarImage
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
import type { BadgeProps } from '@/components/ui/badge';
|
||||
import { Badge as UIBadge } from '@/components/ui/badge';
|
||||
|
||||
export const Badge = UIBadge;
|
||||
export type { BadgeProps };
|
@ -1,15 +0,0 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type ComponentPropsWithoutRef, type ElementType } from 'react';
|
||||
|
||||
type BoxProps<T extends ElementType> = {
|
||||
component?: T;
|
||||
} & ComponentPropsWithoutRef<T>;
|
||||
|
||||
export function Box<T extends ElementType = 'div'>({
|
||||
className,
|
||||
component,
|
||||
...props
|
||||
}: BoxProps<T>) {
|
||||
const Component = component || 'div';
|
||||
return <Component className={cn(className)} {...props} />;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { Button as UIButton, type ButtonProps } from '@/components/ui/button';
|
||||
|
||||
// Re-export the UI Button directly
|
||||
export const Button = UIButton;
|
||||
export type { ButtonProps };
|
@ -1,5 +0,0 @@
|
||||
import type { CalendarProps } from '@/components/ui/calendar';
|
||||
import { Calendar as UICalendar } from '@/components/ui/calendar';
|
||||
|
||||
export const Calendar = UICalendar;
|
||||
export type { CalendarProps };
|
@ -1,6 +0,0 @@
|
||||
import { Checkbox as UICheckbox } from '@/components/ui/checkbox';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
|
||||
export const Checkbox = UICheckbox;
|
||||
export type CheckboxProps = ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>;
|
@ -1,44 +0,0 @@
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { format } from 'date-fns';
|
||||
import { CalendarIcon } from 'lucide-react';
|
||||
import { Button } from './Button';
|
||||
import { Calendar } from './Calendar';
|
||||
|
||||
interface DatePickerProps {
|
||||
date?: Date;
|
||||
onChange?: (date?: Date) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function DatePicker({ date, onChange, className }: DatePickerProps) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'w-full justify-start text-left font-normal',
|
||||
!date && 'text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{date ? format(date, 'PPP') : <span>Pick a date</span>}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={onChange}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import * as React from 'react';
|
||||
|
||||
interface FormControlProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
export function FormControl({ className, error, ...props }: FormControlProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'space-y-2',
|
||||
error && 'text-destructive',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormHelperText({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
'text-sm text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type LucideIcon } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
interface IconWithFrameProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
icon: LucideIcon;
|
||||
variant?: 'default' | 'success' | 'warning' | 'danger';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export function IconWithFrame({
|
||||
icon: Icon,
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
className,
|
||||
...props
|
||||
}: IconWithFrameProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center rounded-lg',
|
||||
size === 'sm' && 'h-8 w-8',
|
||||
size === 'md' && 'h-10 w-10',
|
||||
size === 'lg' && 'h-12 w-12',
|
||||
variant === 'default' && 'bg-muted text-foreground',
|
||||
variant === 'success' && 'bg-success/20 text-success',
|
||||
variant === 'warning' && 'bg-warning/20 text-warning',
|
||||
variant === 'danger' && 'bg-destructive/20 text-destructive',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Icon
|
||||
className={cn(
|
||||
size === 'sm' && 'h-4 w-4',
|
||||
size === 'md' && 'h-5 w-5',
|
||||
size === 'lg' && 'h-6 w-6'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { Input as UIInput } from '@/components/ui/input';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
export const Input = UIInput;
|
||||
export type InputProps = ComponentProps<'input'>;
|
@ -1,11 +0,0 @@
|
||||
import {
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
InputOTP as UIInputOTP,
|
||||
} from '@/components/ui/input-otp';
|
||||
|
||||
export const InputOTP = UIInputOTP;
|
||||
export {
|
||||
InputOTPGroup,
|
||||
InputOTPSlot
|
||||
};
|
@ -1,94 +0,0 @@
|
||||
import {
|
||||
Dialog as UIDialog,
|
||||
DialogContent as UIDialogContent,
|
||||
DialogDescription as UIDialogDescription,
|
||||
DialogFooter as UIDialogFooter,
|
||||
DialogHeader as UIDialogHeader,
|
||||
DialogTitle as UIDialogTitle,
|
||||
DialogTrigger as UIDialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { cn } from '@/lib/utils';
|
||||
import * as React from 'react';
|
||||
|
||||
interface ModalProps {
|
||||
open?: boolean;
|
||||
onClose?: () => void;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Main Modal component that matches MUI's Modal API
|
||||
export function Modal({ open, onClose, children }: ModalProps) {
|
||||
return (
|
||||
<UIDialog open={open} onOpenChange={(isOpen) => !isOpen && onClose?.()}>
|
||||
{children}
|
||||
</UIDialog>
|
||||
);
|
||||
}
|
||||
|
||||
// Box-like wrapper for modal content to match MUI's style
|
||||
export function ModalContent({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<UIDialogContent className={cn("gap-0 p-0", className)} {...props}>
|
||||
{children}
|
||||
</UIDialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
interface ModalTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ModalTitle({ children, className, ...props }: ModalTitleProps) {
|
||||
return (
|
||||
<UIDialogTitle className={cn("px-6 py-4", className)} {...props}>
|
||||
{children}
|
||||
</UIDialogTitle>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModalDescription({ children, className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
||||
return (
|
||||
<UIDialogDescription className={cn("px-6", className)} {...props}>
|
||||
{children}
|
||||
</UIDialogDescription>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModalFooter({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<UIDialogFooter className={cn("px-6 py-4", className)} {...props}>
|
||||
{children}
|
||||
</UIDialogFooter>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModalHeader({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<UIDialogHeader className={cn("px-6 py-4", className)} {...props}>
|
||||
{children}
|
||||
</UIDialogHeader>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModalTrigger({ children, className, ...props }: React.ComponentProps<typeof UIDialogTrigger>) {
|
||||
return (
|
||||
<UIDialogTrigger className={cn(className)} {...props}>
|
||||
{children}
|
||||
</UIDialogTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
// For backwards compatibility with MUI Dialog naming
|
||||
export const Dialog = Modal;
|
||||
export const DialogContent = ModalContent;
|
||||
export const DialogTitle = ModalTitle;
|
||||
export const DialogDescription = ModalDescription;
|
||||
export const DialogFooter = ModalFooter;
|
||||
export const DialogHeader = ModalHeader;
|
||||
export const DialogTrigger = ModalTrigger;
|
||||
export const DialogActions = DialogFooter; // MUI specific alias
|
@ -1,51 +0,0 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import * as React from 'react';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './Tooltip';
|
||||
|
||||
interface OverflownTextProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
text: string;
|
||||
maxWidth?: string | number;
|
||||
}
|
||||
|
||||
export function OverflownText({ text, maxWidth = '100%', className, ...props }: OverflownTextProps) {
|
||||
const [isOverflown, setIsOverflown] = React.useState(false);
|
||||
const textRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const element = textRef.current;
|
||||
if (element) {
|
||||
setIsOverflown(element.scrollWidth > element.clientWidth);
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
const content = (
|
||||
<div
|
||||
ref={textRef}
|
||||
className={cn(
|
||||
'truncate',
|
||||
className
|
||||
)}
|
||||
style={{ maxWidth }}
|
||||
{...props}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!isOverflown) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{content}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-xs break-words">{text}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
|
||||
export const Radio = RadioGroup;
|
||||
export const RadioItem = RadioGroupItem;
|
||||
export type RadioGroupProps = ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>;
|
||||
export type RadioGroupItemProps = ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>;
|
@ -1,19 +0,0 @@
|
||||
import {
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Select as UISelect,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
export const Select = UISelect;
|
||||
export {
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
import { Separator as UISeparator } from '@/components/ui/separator';
|
||||
|
||||
export const Separator = UISeparator;
|
@ -1,18 +0,0 @@
|
||||
import {
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
Sheet as UISheet,
|
||||
} from '@/components/ui/sheet';
|
||||
|
||||
export const Sheet = UISheet;
|
||||
export {
|
||||
SheetClose, SheetContent,
|
||||
SheetDescription, SheetFooter, SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger
|
||||
};
|
@ -1,6 +0,0 @@
|
||||
import { Switch as UISwitch } from '@/components/ui/switch';
|
||||
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
|
||||
export const Switch = UISwitch;
|
||||
export type SwitchProps = ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>;
|
@ -1,21 +0,0 @@
|
||||
import {
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Table as UITable,
|
||||
} from '@/components/ui/table';
|
||||
|
||||
export const Table = UITable;
|
||||
export {
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
import {
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
Tabs as UITabs,
|
||||
} from '@/components/ui/tabs';
|
||||
|
||||
export const Tabs = UITabs;
|
||||
export {
|
||||
TabsContent, TabsList,
|
||||
TabsTrigger
|
||||
};
|
@ -1,60 +0,0 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type ComponentProps, type CSSProperties, type ReactNode } from 'react';
|
||||
|
||||
type BadgeProps = ComponentProps<typeof Badge>;
|
||||
|
||||
interface TagProps extends Omit<BadgeProps, 'variant' | 'style'> {
|
||||
variant?: 'default' | 'success' | 'warning' | 'danger' | 'outline';
|
||||
type?: 'attention' | 'negative' | 'positive' | 'emphasized' | 'neutral';
|
||||
tagStyle?: 'default' | 'minimal';
|
||||
size?: 'sm' | 'xs';
|
||||
leftIcon?: ReactNode;
|
||||
rightIcon?: ReactNode;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export function Tag({
|
||||
className,
|
||||
variant,
|
||||
type = 'attention',
|
||||
tagStyle = 'default',
|
||||
size = 'sm',
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
children,
|
||||
style,
|
||||
...props
|
||||
}: TagProps) {
|
||||
// Map old type to new variant if variant not explicitly set
|
||||
const mappedVariant = variant || {
|
||||
attention: 'warning',
|
||||
negative: 'danger',
|
||||
positive: 'success',
|
||||
emphasized: 'default',
|
||||
neutral: 'default',
|
||||
}[type] || 'default';
|
||||
|
||||
return (
|
||||
<Badge
|
||||
{...props}
|
||||
style={style}
|
||||
variant={mappedVariant === 'default' ? 'default' : 'secondary'}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5',
|
||||
size === 'xs' && 'px-2 py-1 text-xs',
|
||||
size === 'sm' && 'px-2 py-2 text-sm',
|
||||
tagStyle === 'minimal' && 'bg-muted border-border',
|
||||
mappedVariant === 'success' && 'bg-success/10 text-success border-success/20',
|
||||
mappedVariant === 'warning' && 'bg-warning/10 text-warning border-warning/20',
|
||||
mappedVariant === 'danger' && 'bg-destructive/10 text-destructive border-destructive/20',
|
||||
mappedVariant === 'outline' && 'bg-background text-foreground border-border',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{leftIcon}
|
||||
{children}
|
||||
{rightIcon}
|
||||
</Badge>
|
||||
);
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import type { ToastActionElement, ToastProps } from '@/components/ui/toast';
|
||||
import { toast, useToast as useUIToast } from '@/hooks/use-toast';
|
||||
|
||||
export const useToast = useUIToast;
|
||||
export { toast };
|
||||
export type { ToastActionElement, ToastProps };
|
||||
|
@ -1,7 +0,0 @@
|
||||
import {
|
||||
ToggleGroupItem,
|
||||
ToggleGroup as UIToggleGroup,
|
||||
} from '@/components/ui/toggle-group';
|
||||
|
||||
export const ToggleGroup = UIToggleGroup;
|
||||
export { ToggleGroupItem };
|
@ -1,13 +0,0 @@
|
||||
import {
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
Tooltip as UITooltip,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
export const Tooltip = UITooltip;
|
||||
export {
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger
|
||||
};
|
@ -1,91 +0,0 @@
|
||||
import { Check } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from '@/components/ui/command';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './Avatar';
|
||||
import { Button } from './Button';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
interface UserSelectProps {
|
||||
users: User[];
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export function UserSelect({ users, value, onChange, placeholder = 'Select user...' }: UserSelectProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const selectedUser = users.find(user => user.id === value);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{selectedUser ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={selectedUser.avatar} />
|
||||
<AvatarFallback>{selectedUser.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span>{selectedUser.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
placeholder
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search users..." />
|
||||
<CommandEmpty>No users found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{users.map(user => (
|
||||
<CommandItem
|
||||
key={user.id}
|
||||
onSelect={() => {
|
||||
onChange?.(user.id);
|
||||
setOpen(false);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback>{user.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col">
|
||||
<span>{user.name}</span>
|
||||
<span className="text-sm text-muted-foreground">{user.email}</span>
|
||||
</div>
|
||||
{value === user.id && (
|
||||
<Check className="ml-auto h-4 w-4" />
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
57
packages/frontend/src/components/examples/ExamplePage.tsx
Normal file
57
packages/frontend/src/components/examples/ExamplePage.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { ReactNode } from 'react';
|
||||
import {
|
||||
Header,
|
||||
NavigationWrapper,
|
||||
ScreenWrapper,
|
||||
TabWrapper,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '../layout';
|
||||
|
||||
interface ExamplePageProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function ExamplePage({ children }: ExamplePageProps) {
|
||||
return (
|
||||
<NavigationWrapper>
|
||||
<ScreenWrapper>
|
||||
<Header
|
||||
title="Example Page"
|
||||
subtitle="Demonstrating the standard layout components"
|
||||
actions={[
|
||||
<button key="action" className="btn-primary">
|
||||
Action
|
||||
</button>,
|
||||
]}
|
||||
/>
|
||||
|
||||
<TabWrapper defaultValue="tab1">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1">Overview</TabsTrigger>
|
||||
<TabsTrigger value="tab2">Details</TabsTrigger>
|
||||
<TabsTrigger value="tab3">Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="tab1">
|
||||
<div className="p-4">
|
||||
<h2>Overview Content</h2>
|
||||
{children}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="tab2">
|
||||
<div className="p-4">
|
||||
<h2>Details Content</h2>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="tab3">
|
||||
<div className="p-4">
|
||||
<h2>Settings Content</h2>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabWrapper>
|
||||
</ScreenWrapper>
|
||||
</NavigationWrapper>
|
||||
);
|
||||
}
|
1
packages/frontend/src/components/index.ts
Normal file
1
packages/frontend/src/components/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './ui';
|
9
packages/frontend/src/components/layout/index.ts
Normal file
9
packages/frontend/src/components/layout/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export { NavigationWrapper } from './navigation';
|
||||
export { Header } from './screen-header/Header';
|
||||
export { ScreenWrapper } from './screen-wrapper/ScreenWrapper';
|
||||
export { TabWrapper } from './screen-wrapper/TabWrapper';
|
||||
// Re-export tab components for convenience
|
||||
export {
|
||||
TabsContent, TabsList,
|
||||
TabsTrigger
|
||||
} from '@/components/ui/tabs';
|
@ -0,0 +1,31 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TopNavigation } from './TopNavigation';
|
||||
|
||||
/**
|
||||
* NavigationWrapperProps interface extends React.HTMLProps<HTMLDivElement> to include all standard HTML div attributes.
|
||||
*/
|
||||
interface NavigationWrapperProps extends React.HTMLProps<HTMLDivElement> {}
|
||||
|
||||
/**
|
||||
* Wraps the application's navigation and content within a consistent layout.
|
||||
* It provides a basic structure with a top navigation bar and a content area.
|
||||
*
|
||||
* @param {NavigationWrapperProps} props - The props passed to the NavigationWrapper component.
|
||||
* @param {React.ReactNode} props.children - The content to be rendered within the wrapper.
|
||||
* @param {string} [props.className] - Optional CSS class names to apply to the wrapper.
|
||||
* @param {React.HTMLAttributes<HTMLDivElement>} props.props - Other standard HTML attributes.
|
||||
*
|
||||
* @returns {React.ReactElement} A div element containing the top navigation and the children.
|
||||
*/
|
||||
export function NavigationWrapper({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: NavigationWrapperProps) {
|
||||
return (
|
||||
<div className={cn('min-h-screen bg-background', className)} {...props}>
|
||||
<TopNavigation />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { LaconicMark } from '@/laconic-assets/laconic-mark';
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
import { Menu, Shapes, Wallet } from 'lucide-react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { ProjectSearchBar } from '../search/ProjectSearchBar';
|
||||
import { ColorModeToggle } from './components/ColorModeToggle';
|
||||
import { GitHubSessionButton } from './components/GitHubSessionButton';
|
||||
import { WalletSessionId } from './components/WalletSessionId';
|
||||
|
||||
/**
|
||||
* TopNavigation Component
|
||||
*
|
||||
* Renders the top navigation bar, adapting its layout for desktop and mobile views.
|
||||
* It includes the project search bar, navigation links, user authentication via GitHub,
|
||||
* color mode toggle, and wallet session information. On mobile, it utilizes a sheet
|
||||
* for a responsive and accessible navigation experience.
|
||||
*
|
||||
* @returns {React.ReactElement} A PopoverPrimitive.Root element containing the top navigation bar.
|
||||
*/
|
||||
export function TopNavigation() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<PopoverPrimitive.Root>
|
||||
<div className="bg-background">
|
||||
{/* Top Navigation - Desktop */}
|
||||
<nav className="md:flex items-center justify-between hidden h-16 px-6 border-b">
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Logo / Home Link */}
|
||||
{/* <Button
|
||||
variant="ghost"
|
||||
asChild
|
||||
className="hover:bg-transparent p-0"
|
||||
> */}
|
||||
<Link to="/" className="flex items-center justify-center w-10 h-10">
|
||||
<LaconicMark />
|
||||
</Link>
|
||||
{/* </Button> */}
|
||||
|
||||
{/* Search Bar */}
|
||||
{/* <div className="w-96">
|
||||
<ProjectSearchBar
|
||||
onChange={(project) => {
|
||||
navigate(
|
||||
`/${project.organization.slug}/projects/${project.id}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
{/* Navigation Items */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/deploy-tools/projects">
|
||||
<Button variant="ghost" asChild className="gap-2">
|
||||
<Shapes className="w-5 h-5" />
|
||||
<span>Projects</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="ghost"
|
||||
asChild
|
||||
className="text-muted-foreground gap-2"
|
||||
>
|
||||
<Link to="/wallet">
|
||||
<Button asChild variant="ghost">
|
||||
<Wallet className="w-5 h-5" />
|
||||
<span>Wallet</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{/* <NavigationActions /> */}
|
||||
<Button variant="ghost" asChild className="text-muted-foreground">
|
||||
<Link to="/support">Support</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="text-muted-foreground">
|
||||
<Link to="/docs">Documentation</Link>
|
||||
</Button>
|
||||
<GitHubSessionButton />
|
||||
<ColorModeToggle />
|
||||
<WalletSessionId walletId="0xAb...1234" />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Top Navigation - Mobile */}
|
||||
<nav className="md:hidden flex items-center justify-between h-16 px-4 border-b">
|
||||
<Sheet>
|
||||
<SheetTrigger>
|
||||
<Button asChild variant="outline" size="icon">
|
||||
<div>
|
||||
<Menu className="w-4 h-4" />
|
||||
</div>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
side="left"
|
||||
className="w-[300px] sm:w-[400px] flex flex-col"
|
||||
>
|
||||
<nav className="flex flex-col flex-grow space-y-4">
|
||||
<div className="px-4 py-2">
|
||||
<ProjectSearchBar
|
||||
onChange={(project) => {
|
||||
navigate(
|
||||
`/${project.organization.slug}/projects/${project.id}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="ghost" asChild className="justify-start gap-2">
|
||||
<Link to="/projects">
|
||||
<Shapes className="w-5 h-5" />
|
||||
<span>Projects</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="justify-start gap-2">
|
||||
<Link to="/wallet">
|
||||
<Wallet className="w-5 h-5" />
|
||||
<span>Wallet</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="justify-start">
|
||||
<Link to="/support">Support</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="justify-start">
|
||||
<Link to="/docs">Documentation</Link>
|
||||
</Button>
|
||||
</nav>
|
||||
<div className="flex items-center justify-between mt-auto">
|
||||
<GitHubSessionButton />
|
||||
<ColorModeToggle />
|
||||
<WalletSessionId walletId="0xAb...1234" />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<div className="flex items-center gap-2">
|
||||
<ColorModeToggle />
|
||||
<WalletSessionId walletId="0xAb...1234" />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</PopoverPrimitive.Root>
|
||||
);
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
export function ColorModeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
function toggleTheme() {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="outline" size="icon" onClick={toggleTheme}>
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useGitHubAuth } from '@/hooks/useGitHubAuth';
|
||||
import { Github } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Renders a button that allows users to log in or log out with their GitHub account.
|
||||
* It displays the user's avatar and username when logged in, and provides a logout option.
|
||||
*
|
||||
* @returns {React.ReactElement} A DropdownMenu element containing the GitHub session button.
|
||||
*/
|
||||
export function GitHubSessionButton() {
|
||||
const { isAuthenticated, user, login, logout } = useGitHubAuth();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={`relative ${isAuthenticated ? 'text-green-500' : 'text-muted-foreground'}`}
|
||||
>
|
||||
<Github className="h-[1.2rem] w-[1.2rem]" />
|
||||
{isAuthenticated && (
|
||||
<span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-green-500" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent align="end" className="w-56" sideOffset={4}>
|
||||
{isAuthenticated && user ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2 p-2">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={user.avatar_url} alt={user.login} />
|
||||
<AvatarFallback>
|
||||
{user.login.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">
|
||||
{user.name || user.login}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{user.login}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuItem onClick={logout}>Log out</DropdownMenuItem>
|
||||
</>
|
||||
) : (
|
||||
<DropdownMenuItem onClick={login}>
|
||||
Log in with GitHub
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
@ -1,12 +1,36 @@
|
||||
import type React from "react"
|
||||
import type React from 'react';
|
||||
|
||||
/**
|
||||
* LaconicIconProps interface defines the props for the LaconicIcon component.
|
||||
*/
|
||||
interface LaconicIconProps {
|
||||
className?: string
|
||||
width?: number
|
||||
height?: number
|
||||
/**
|
||||
* Optional CSS class names to apply to the component.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The width of the icon.
|
||||
* @default 40
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* The height of the icon.
|
||||
* @default 40
|
||||
*/
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export const LaconicIcon: React.FC<LaconicIconProps> = ({ className = "", width = 40, height = 40 }) => {
|
||||
/**
|
||||
* A component that renders the Laconic icon.
|
||||
*
|
||||
* @param {LaconicIconProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} An SVG element representing the Laconic icon.
|
||||
*/
|
||||
export const LaconicIcon: React.FC<LaconicIconProps> = ({
|
||||
className = '',
|
||||
width = 40,
|
||||
height = 40,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
@ -23,6 +47,5 @@ export const LaconicIcon: React.FC<LaconicIconProps> = ({ className = "", width
|
||||
className="fill-current"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
);
|
||||
};
|
@ -0,0 +1,44 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useGQLClient } from '@/context/GQLClientContext';
|
||||
import { Bell, Plus } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Renders the navigation actions, including buttons for creating a new project and displaying notifications.
|
||||
*
|
||||
* @returns {React.ReactElement} A div element containing the navigation actions.
|
||||
*/
|
||||
export function NavigationActions() {
|
||||
const navigate = useNavigate();
|
||||
const client = useGQLClient();
|
||||
|
||||
/**
|
||||
* Fetches the organization slug.
|
||||
* @returns {Promise<string>} The organization slug.
|
||||
*/
|
||||
const fetchOrgSlug = useCallback(async () => {
|
||||
const { organizations } = await client.getOrganizations();
|
||||
// TODO: Get the selected organization. This is temp
|
||||
return organizations[0].slug;
|
||||
}, [client]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
fetchOrgSlug().then((organizationSlug) => {
|
||||
navigate(`/${organizationSlug}/projects/create`);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Bell className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,16 +1,31 @@
|
||||
import type React from 'react';
|
||||
|
||||
/**
|
||||
* WalletSessionIdProps interface defines the props for the WalletSessionId component.
|
||||
*/
|
||||
interface WalletSessionIdProps {
|
||||
/**
|
||||
* The wallet ID to display.
|
||||
*/
|
||||
walletId?: string;
|
||||
/**
|
||||
* Optional CSS class names to apply to the component.
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that displays the wallet session ID.
|
||||
*
|
||||
* @param {WalletSessionIdProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} A div element containing the wallet session ID.
|
||||
*/
|
||||
export const WalletSessionId: React.FC<WalletSessionIdProps> = ({
|
||||
walletId,
|
||||
className = '',
|
||||
}) => {
|
||||
// const { wallet } = useWallet();
|
||||
const wallet = {id: 'x123xxx'}
|
||||
const wallet = { id: 'x123xxx' };
|
||||
const displayId = walletId || wallet?.id || 'Not Connected';
|
||||
|
||||
return (
|
@ -0,0 +1,9 @@
|
||||
export { ProjectSearchBar } from '../search/ProjectSearchBar';
|
||||
export { ColorModeToggle } from './components/ColorModeToggle';
|
||||
export { GitHubSessionButton } from './components/GitHubSessionButton';
|
||||
export { LaconicIcon } from './components/LaconicIcon';
|
||||
export { NavigationActions } from './components/NavigationActions';
|
||||
export { WalletSessionId } from './components/WalletSessionId';
|
||||
export { NavigationWrapper } from './NavigationWrapper';
|
||||
export { TopNavigation } from './TopNavigation';
|
||||
|
@ -0,0 +1,56 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* ActionButtonProps interface defines the props for the ActionButton component.
|
||||
*/
|
||||
interface ActionButtonProps {
|
||||
/**
|
||||
* The label of the button.
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* The icon to display in the button.
|
||||
*/
|
||||
icon: LucideIcon;
|
||||
/**
|
||||
* The variant of the button.
|
||||
* @default 'default'
|
||||
*/
|
||||
variant?: 'default' | 'outline';
|
||||
/**
|
||||
* Callback function to be called when the button is clicked.
|
||||
*/
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A button component that displays an icon and a label, and triggers an action when clicked.
|
||||
*
|
||||
* @param {ActionButtonProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} A Button element.
|
||||
*/
|
||||
export function ActionButton({
|
||||
label,
|
||||
icon: Icon,
|
||||
variant = 'default',
|
||||
onClick,
|
||||
}: ActionButtonProps) {
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleClick = () => {
|
||||
onClick?.();
|
||||
toast({
|
||||
title: 'Action Triggered',
|
||||
description: 'TODO: Connect action',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Button variant={variant} onClick={handleClick} className="gap-2">
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{label}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
/**
|
||||
* HeaderProps interface defines the props for the Header component.
|
||||
*/
|
||||
interface HeaderProps {
|
||||
/**
|
||||
* The title of the header.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* The subtitle of the header.
|
||||
*/
|
||||
subtitle?: string;
|
||||
/**
|
||||
* An array of actions to display in the header.
|
||||
*/
|
||||
actions?: ReactNode[];
|
||||
/**
|
||||
* A back button to display in the header.
|
||||
*/
|
||||
backButton?: ReactNode;
|
||||
/**
|
||||
* Optional CSS class names to apply to the header.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The layout of the header.
|
||||
* @default 'default'
|
||||
*/
|
||||
layout?: 'default' | 'compact';
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that renders a header with a title, subtitle, actions, and back button.
|
||||
*
|
||||
* @param {HeaderProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} A div element containing the header content.
|
||||
*/
|
||||
export function Header({
|
||||
title,
|
||||
subtitle,
|
||||
actions,
|
||||
backButton,
|
||||
className,
|
||||
layout = 'default',
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col sm:flex-row sm:items-center sm:justify-between',
|
||||
layout === 'compact' ? 'mb-2' : 'mb-4',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{backButton}
|
||||
<div>
|
||||
<h1
|
||||
className={cn(
|
||||
'font-bold',
|
||||
layout === 'compact'
|
||||
? 'text-lg sm:text-xl'
|
||||
: 'text-xl sm:text-3xl',
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{subtitle && (
|
||||
<p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{actions && actions.length > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2',
|
||||
layout === 'default' ? 'mt-4 sm:mt-0' : 'mt-2 sm:mt-0',
|
||||
)}
|
||||
>
|
||||
{actions.map((action, index) => (
|
||||
<div key={index}>{action}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* ScreenWrapperProps interface extends React.HTMLProps<HTMLDivElement> to include all standard HTML div attributes.
|
||||
*/
|
||||
interface ScreenWrapperProps extends React.HTMLProps<HTMLDivElement> {
|
||||
/**
|
||||
* Whether the screen should be padded.
|
||||
* @default true
|
||||
*/
|
||||
padded?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper component for screens, providing consistent padding and layout.
|
||||
*
|
||||
* @param {ScreenWrapperProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} A div element containing the screen content.
|
||||
*/
|
||||
export function ScreenWrapper({
|
||||
children,
|
||||
className,
|
||||
// padded = true,
|
||||
...props
|
||||
}: ScreenWrapperProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-1 flex-col',
|
||||
'container mx-auto p-4 md:p-6 lg:p-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* TabWrapperProps interface extends React.ComponentProps<typeof Tabs> to include all standard Tabs attributes.
|
||||
*/
|
||||
interface TabWrapperProps extends React.ComponentProps<typeof Tabs> {
|
||||
/**
|
||||
* The orientation of the tabs.
|
||||
* @default 'horizontal'
|
||||
*/
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper component for the Tabs component from `ui/tabs`.
|
||||
*
|
||||
* @param {TabWrapperProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} A Tabs element containing the tabs and content.
|
||||
*/
|
||||
export function TabWrapper({
|
||||
children,
|
||||
className,
|
||||
orientation = 'horizontal',
|
||||
...props
|
||||
}: TabWrapperProps) {
|
||||
return (
|
||||
<Tabs
|
||||
{...props}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'w-full',
|
||||
orientation === 'vertical' && 'flex gap-6',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
// Re-export tab components for convenience
|
||||
export { TabsContent, TabsList, TabsTrigger };
|
@ -0,0 +1,106 @@
|
||||
import { useCombobox } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useDebounceValue } from 'usehooks-ts';
|
||||
|
||||
import SearchBar from '@/components/SearchBar';
|
||||
import { useGQLClient } from '@/context/GQLClientContext';
|
||||
import { cn } from '@/utils/classnames';
|
||||
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
|
||||
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
|
||||
|
||||
/**
|
||||
* ProjectSearchBarProps interface defines the props for the ProjectSearchBar component.
|
||||
*/
|
||||
interface ProjectSearchBarProps {
|
||||
/**
|
||||
* Callback function to be called when a project is selected.
|
||||
* @param data - The selected project data.
|
||||
*/
|
||||
onChange?: (data: Project) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A search bar component that allows the user to search for projects.
|
||||
*
|
||||
* @param {ProjectSearchBarProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} A div element containing the search bar and project list.
|
||||
*/
|
||||
export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
|
||||
const [items, setItems] = useState<Project[]>([]);
|
||||
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
|
||||
const client = useGQLClient();
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
getMenuProps,
|
||||
getInputProps,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
inputValue,
|
||||
} = useCombobox({
|
||||
items,
|
||||
itemToString(item) {
|
||||
return item ? item.name : '';
|
||||
},
|
||||
selectedItem,
|
||||
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
|
||||
if (newSelectedItem) {
|
||||
setSelectedItem(newSelectedItem);
|
||||
|
||||
if (onChange) {
|
||||
onChange(newSelectedItem);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300);
|
||||
|
||||
const fetchProjects = useCallback(
|
||||
async (inputValue: string) => {
|
||||
const { searchProjects } = await client.searchProjects(inputValue);
|
||||
setItems(searchProjects);
|
||||
},
|
||||
[client],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedInputValue) {
|
||||
fetchProjects(debouncedInputValue);
|
||||
}
|
||||
}, [fetchProjects, debouncedInputValue]);
|
||||
|
||||
return (
|
||||
<div className="relative w-full lg:w-fit dark:bg-overlay">
|
||||
<SearchBar {...getInputProps()} />
|
||||
<div
|
||||
{...getMenuProps({}, { suppressRefError: true })}
|
||||
className={cn(
|
||||
'flex flex-col shadow-dropdown rounded-xl dark:bg-overlay2 bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
|
||||
{ hidden: !inputValue || !isOpen },
|
||||
)}
|
||||
>
|
||||
{items.length ? (
|
||||
<>
|
||||
<div className="px-2 py-2">
|
||||
<p className="text-elements-mid-em text-xs font-medium">
|
||||
Suggestions
|
||||
</p>
|
||||
</div>
|
||||
{items.map((item, index) => (
|
||||
<ProjectSearchBarItem
|
||||
{...getItemProps({ item, index })}
|
||||
key={item.id}
|
||||
item={item}
|
||||
active={highlightedIndex === index || selectedItem === item}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<ProjectSearchBarEmpty />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,146 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { IconInput } from '@/components/ui/extended/input-w-icons';
|
||||
import { useGQLClient } from '@/context/GQLClientContext';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { useCombobox } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import { Search, X } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDebounceValue } from 'usehooks-ts';
|
||||
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
|
||||
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
|
||||
|
||||
/**
|
||||
* ProjectSearchBarDialogProps interface defines the props for the ProjectSearchBarDialog component.
|
||||
*/
|
||||
interface ProjectSearchBarDialogProps extends Dialog.DialogProps {
|
||||
/**
|
||||
* Whether the dialog is open.
|
||||
*/
|
||||
open?: boolean;
|
||||
/**
|
||||
* Callback function to be called when the dialog is closed.
|
||||
*/
|
||||
onClose?: () => void;
|
||||
/**
|
||||
* Callback function to be called when a project item is clicked.
|
||||
* @param data - The selected project data.
|
||||
*/
|
||||
onClickItem?: (data: Project) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dialog component that allows the user to search for projects.
|
||||
*
|
||||
* @param {ProjectSearchBarDialogProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} A Dialog.Root element containing the search bar and project list.
|
||||
*/
|
||||
export const ProjectSearchBarDialog = ({
|
||||
onClose,
|
||||
onClickItem,
|
||||
...props
|
||||
}: ProjectSearchBarDialogProps) => {
|
||||
const [items, setItems] = useState<Project[]>([]);
|
||||
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
|
||||
const client = useGQLClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
getInputProps,
|
||||
getItemProps,
|
||||
getMenuProps,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
} = useCombobox({
|
||||
items,
|
||||
itemToString(item) {
|
||||
return item ? item.name : '';
|
||||
},
|
||||
selectedItem,
|
||||
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
|
||||
if (newSelectedItem) {
|
||||
setSelectedItem(newSelectedItem);
|
||||
onClickItem?.(newSelectedItem);
|
||||
navigate(
|
||||
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300);
|
||||
|
||||
const fetchProjects = useCallback(
|
||||
async (inputValue: string) => {
|
||||
const { searchProjects } = await client.searchProjects(inputValue);
|
||||
setItems(searchProjects);
|
||||
},
|
||||
[client],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedInputValue) {
|
||||
fetchProjects(debouncedInputValue);
|
||||
}
|
||||
}, [fetchProjects, debouncedInputValue]);
|
||||
|
||||
const handleClose = () => {
|
||||
setInputValue('');
|
||||
setItems([]);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog.Root {...props}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="bg-base-bg md:hidden fixed inset-0 overflow-y-auto" />
|
||||
<Dialog.Content>
|
||||
<div className="fixed inset-0 top-0 flex flex-col h-full">
|
||||
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
|
||||
<IconInput
|
||||
{...getInputProps({}, { suppressRefError: true })}
|
||||
leftIcon={<Search />}
|
||||
placeholder="Search"
|
||||
className="border-none shadow-none"
|
||||
autoFocus
|
||||
/>
|
||||
<Button size="icon" variant="ghost" onClick={handleClose}>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div
|
||||
className="flex flex-col gap-1 px-2 py-2"
|
||||
{...getMenuProps(
|
||||
{},
|
||||
{
|
||||
suppressRefError: true,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{items.length > 0 ? (
|
||||
<>
|
||||
<div className="px-2 py-2">
|
||||
<p className="text-elements-mid-em text-xs font-medium">
|
||||
Suggestions
|
||||
</p>
|
||||
</div>
|
||||
{items.map((item, index) => (
|
||||
<ProjectSearchBarItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
{...getItemProps({ item, index })}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
inputValue && <ProjectSearchBarEmpty />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
import { cn } from '@/utils/classnames';
|
||||
import { Search } from 'lucide-react';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
|
||||
/**
|
||||
* ProjectSearchBarEmptyProps interface defines the props for the ProjectSearchBarEmpty component.
|
||||
*/
|
||||
interface ProjectSearchBarEmptyProps extends ComponentPropsWithoutRef<'div'> {}
|
||||
|
||||
/**
|
||||
* A component that renders a message when no projects match the search query.
|
||||
*
|
||||
* @param {ProjectSearchBarEmptyProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} A div element displaying a "no projects found" message.
|
||||
*/
|
||||
export const ProjectSearchBarEmpty = ({
|
||||
className,
|
||||
...props
|
||||
}: ProjectSearchBarEmptyProps) => {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn('flex items-center px-2 py-2 gap-3', className)}
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-orange-50 text-elements-warning dark:bg-red-50 text-error">
|
||||
<Search size={16} />
|
||||
</div>
|
||||
<p className="text-elements-low-em text-sm dark:text-foreground-secondary tracking-[-0.006em]">
|
||||
No projects matching this name
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,75 @@
|
||||
import { Avatar } from '@/components/shared/Avatar';
|
||||
import { OmitCommon } from '@/types/common';
|
||||
import { cn } from '@/utils/classnames';
|
||||
|
||||
import { Overwrite, UseComboboxGetItemPropsReturnValue } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import { ComponentPropsWithoutRef, forwardRef } from 'react';
|
||||
import { getInitials } from '../../../../utils/geInitials';
|
||||
|
||||
/**
|
||||
* Represents a type that merges ComponentPropsWithoutRef<'li'> with certain exclusions.
|
||||
* @type {MergedComponentPropsWithoutRef}
|
||||
*/
|
||||
type MergedComponentPropsWithoutRef = OmitCommon<
|
||||
ComponentPropsWithoutRef<'button'>,
|
||||
Omit<
|
||||
Overwrite<UseComboboxGetItemPropsReturnValue, Project[]>,
|
||||
'index' | 'item'
|
||||
>
|
||||
>;
|
||||
|
||||
/**
|
||||
* ProjectSearchBarItemProps interface defines the props for the ProjectSearchBarItem component.
|
||||
*/
|
||||
interface ProjectSearchBarItemProps extends MergedComponentPropsWithoutRef {
|
||||
/**
|
||||
* The project item to display.
|
||||
*/
|
||||
item: Project;
|
||||
/**
|
||||
* Whether the item is active.
|
||||
*/
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that renders a project item in the search bar.
|
||||
*
|
||||
* @param {ProjectSearchBarItemProps} props - The props for the component.
|
||||
* @returns {React.ReactElement} A button element representing a project item.
|
||||
*/
|
||||
const ProjectSearchBarItem = forwardRef<
|
||||
HTMLButtonElement,
|
||||
ProjectSearchBarItemProps
|
||||
>(({ item, active, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
ref={ref}
|
||||
key={item.id}
|
||||
className={cn(
|
||||
'px-2 py-2 flex items-center gap-3 rounded-lg text-left hover:bg-base-bg-emphasized',
|
||||
{
|
||||
'bg-base-bg-emphasized': active,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
size={32}
|
||||
imageSrc={item.icon}
|
||||
initials={getInitials(item.name)}
|
||||
/>
|
||||
<div className="flex flex-col flex-1">
|
||||
<p className="text-sm tracking-[-0.006em] text-elements-high-em">
|
||||
{item.name}
|
||||
</p>
|
||||
<p className="text-elements-low-em text-xs">{item.organization.name}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
ProjectSearchBarItem.displayName = 'ProjectSearchBarItem';
|
||||
|
||||
export { ProjectSearchBarItem };
|
@ -0,0 +1,2 @@
|
||||
export * from './ProjectSearchBar';
|
||||
export * from './ProjectSearchBarDialog';
|
83
packages/frontend/src/components/loading/loading-overlay.tsx
Normal file
83
packages/frontend/src/components/loading/loading-overlay.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import { LaconicMark } from '@/laconic-assets/laconic-mark';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
export interface LoadingOverlayProps {
|
||||
/**
|
||||
* Controls the visibility of the overlay.
|
||||
* When false, the component returns null.
|
||||
* @default true
|
||||
*/
|
||||
isLoading?: boolean;
|
||||
|
||||
/**
|
||||
* Optional className for styling the overlay container.
|
||||
* This will be merged with the default styles.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Whether to show the Laconic logo in the overlay.
|
||||
* @default true
|
||||
*/
|
||||
showLogo?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to show the loading spinner below the logo.
|
||||
* @default true
|
||||
*/
|
||||
showSpinner?: boolean;
|
||||
|
||||
/**
|
||||
* The z-index value for the overlay.
|
||||
* Adjust this if you need the overlay to appear above or below other elements.
|
||||
* @default 50
|
||||
*/
|
||||
zIndex?: number;
|
||||
|
||||
/**
|
||||
* Whether to use solid black background instead of semi-transparent.
|
||||
* Useful for initial page load and full-screen loading states.
|
||||
* @default false
|
||||
*/
|
||||
solid?: boolean;
|
||||
}
|
||||
|
||||
export function LoadingOverlay({
|
||||
isLoading = true,
|
||||
className,
|
||||
showLogo = true,
|
||||
showSpinner = true,
|
||||
zIndex = 50,
|
||||
solid = false,
|
||||
}: LoadingOverlayProps) {
|
||||
if (!isLoading) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 flex flex-col items-center justify-center',
|
||||
solid ? 'bg-black' : 'bg-black/90 backdrop-blur-sm',
|
||||
className,
|
||||
)}
|
||||
style={{ zIndex }}
|
||||
role="alert"
|
||||
aria-busy="true"
|
||||
aria-label="Loading"
|
||||
>
|
||||
{showLogo && (
|
||||
<div className="animate-fade-in mb-4">
|
||||
<LaconicMark className="text-white" />
|
||||
</div>
|
||||
)}
|
||||
{showSpinner && (
|
||||
<Loader2
|
||||
className="animate-spin w-6 h-6 text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
|
||||
export function ColorModeToggle() {
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
@ -1,46 +0,0 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { useGitHubAuth } from "@/hooks/useGitHubAuth"
|
||||
import { Github } from "lucide-react"
|
||||
|
||||
export function GitHubSessionButton() {
|
||||
const { isAuthenticated, user, login, logout } = useGitHubAuth()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={`relative ${isAuthenticated ? "text-green-500" : "text-muted-foreground"}`}
|
||||
>
|
||||
<Github className="h-[1.2rem] w-[1.2rem]" />
|
||||
{isAuthenticated && <span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-green-500" />}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent align="end" className="w-56" sideOffset={4} >
|
||||
{isAuthenticated && user ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2 p-2">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={user.avatar_url} alt={user.login} />
|
||||
<AvatarFallback>{user.login.slice(0, 2).toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">{user.name || user.login}</span>
|
||||
<span className="text-xs text-muted-foreground">{user.login}</span>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuItem onClick={logout}>Log out</DropdownMenuItem>
|
||||
</>
|
||||
) : (
|
||||
<DropdownMenuItem onClick={login}>Log in with GitHub</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { TopNavigation } from './TopNavigation';
|
||||
|
||||
interface NavigationWrapperProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function NavigationWrapper({ children }: NavigationWrapperProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="fixed top-0 left-0 right-0 z-10">
|
||||
<TopNavigation />
|
||||
</div>
|
||||
{/* <div className="flex-1 mt-16 px-4 py-6"> */}
|
||||
{children || <Outlet />}
|
||||
{/* </div> */}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
"use client"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import { Menu, Shapes, Wallet } from "lucide-react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ColorModeToggle } from "./ColorModeToggle"
|
||||
import { GitHubSessionButton } from "./GitHubSessionButton"
|
||||
import { LaconicIcon } from "./LaconicIcon"
|
||||
import { WalletSessionId } from "./WalletSessionId"
|
||||
|
||||
export function TopNavigation() {
|
||||
// This is a placeholder. In a real app, you'd manage this state with your auth system
|
||||
|
||||
return (
|
||||
<PopoverPrimitive.Root>
|
||||
<div className="bg-background">
|
||||
{/* Top Navigation - Desktop */}
|
||||
<nav className="hidden h-16 border-b md:flex items-center justify-between px-6">
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Logo / Home Link */}
|
||||
<Button variant="ghost" asChild className="p-0 hover:bg-transparent">
|
||||
<Link to="/" className="flex h-12 w-12 items-center justify-center">
|
||||
<LaconicIcon className="text-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{/* Navigation Items */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" asChild className="gap-2">
|
||||
<Link to="/projects">
|
||||
<Shapes className="h-5 w-5" />
|
||||
<span>Projects</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="gap-2 text-muted-foreground">
|
||||
<Link to="/wallet">
|
||||
<Wallet className="h-5 w-5" />
|
||||
<span>Wallet</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" asChild className="text-muted-foreground">
|
||||
<Link to="/support">Support</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="text-muted-foreground">
|
||||
<Link to="/docs">Documentation</Link>
|
||||
</Button>
|
||||
<GitHubSessionButton />
|
||||
<ColorModeToggle />
|
||||
<WalletSessionId walletId="0xAb...1234" />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Top Navigation - Mobile */}
|
||||
<nav className="flex h-16 items-center justify-between border-b px-4 md:hidden">
|
||||
<Sheet>
|
||||
<SheetTrigger >
|
||||
<Button asChild variant="outline" size="icon">
|
||||
<div>
|
||||
<Menu className="h-4 w-4" />
|
||||
</div>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="w-[300px] sm:w-[400px] flex flex-col">
|
||||
<nav className="flex flex-col space-y-4 flex-grow">
|
||||
<Button variant="ghost" asChild className="justify-start gap-2">
|
||||
<Link to="/projects">
|
||||
<Shapes className="h-5 w-5" />
|
||||
<span>Projects</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="justify-start gap-2">
|
||||
<Link to="/wallet">
|
||||
<Wallet className="h-5 w-5" />
|
||||
<span>Wallet</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="justify-start">
|
||||
<Link to="/support">Support</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild className="justify-start">
|
||||
<Link to="/docs">Documentation</Link>
|
||||
</Button>
|
||||
</nav>
|
||||
<div className="mt-auto flex items-center justify-between">
|
||||
<GitHubSessionButton />
|
||||
<ColorModeToggle />
|
||||
<WalletSessionId walletId="0xAb...1234" />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<div className="flex items-center gap-2">
|
||||
<ColorModeToggle />
|
||||
<WalletSessionId walletId="0xAb...1234" />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</PopoverPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
} from '@/components/shared/ConfirmDialog';
|
||||
|
||||
interface CancelDeploymentDialogProps extends ConfirmDialogProps {}
|
||||
|
||||
@ -18,7 +18,7 @@ export const CancelDeploymentDialog = ({
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, cancel deployment"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
confirmButtonProps={{ variant: 'destructive' }}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em tracking-[-0.006em]">
|
||||
This will halt the deployment and you'll have to start the process
|
||||
|
@ -1,17 +1,18 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
} from '@/components/shared/ConfirmDialog';
|
||||
import { Deployment, Domain } from 'gql-client';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import DeploymentDialogBodyCard from 'components/projects/project/deployments/DeploymentDialogBodyCard';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import DeploymentDialogBodyCard from '@/components/projects/project/deployments/DeploymentDialogBodyCard';
|
||||
import { TagProps } from '@/components/shared/Tag';
|
||||
import { Button } from '@/components/ui';
|
||||
import {
|
||||
ChevronDown,
|
||||
Link,
|
||||
ArrowRightCircle,
|
||||
ChevronDown,
|
||||
Link as LinkIcon,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { TagProps } from 'components/shared/Tag';
|
||||
|
||||
interface ChangeStateToProductionDialogProps extends ConfirmDialogProps {
|
||||
deployment: Deployment;
|
||||
@ -55,7 +56,7 @@ export const ChangeStateToProductionDialog = ({
|
||||
),
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-7">
|
||||
<div className="gap-7 flex flex-col">
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm text-elements-high-em tracking-[-0.006em]">
|
||||
Upon confirmation, this deployment will be changed to production.
|
||||
@ -66,7 +67,7 @@ export const ChangeStateToProductionDialog = ({
|
||||
/>
|
||||
{newDeployment && (
|
||||
<>
|
||||
<div className="flex items-center justify-between w-full text-elements-info">
|
||||
<div className="text-elements-info flex items-center justify-between w-full">
|
||||
{Array.from({ length: 7 }).map((_, index) => (
|
||||
<ChevronDown key={index} />
|
||||
))}
|
||||
@ -85,15 +86,16 @@ export const ChangeStateToProductionDialog = ({
|
||||
{domains.length > 0 &&
|
||||
domains.map((value) => {
|
||||
return (
|
||||
<Link to={value.name}>
|
||||
<Button
|
||||
as="a"
|
||||
href={value.name}
|
||||
leftIcon={<Link size={18} />}
|
||||
variant="link"
|
||||
leftIcon={<LinkIcon size={18} />}
|
||||
// variant="link"
|
||||
key={value.id}
|
||||
asChild
|
||||
>
|
||||
{value.name}
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
@ -1,10 +1,7 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import {
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
|
||||
} from '@/components/shared/ConfirmDialog';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
|
||||
interface DeleteDeploymentDialogProps extends ConfirmDialogProps {
|
||||
isConfirmButtonLoading?: boolean;
|
||||
@ -30,7 +27,7 @@ export const DeleteDeploymentDialog = ({
|
||||
}
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{
|
||||
variant: 'danger',
|
||||
variant: 'destructive',
|
||||
disabled: isConfirmButtonLoading,
|
||||
rightIcon: isConfirmButtonLoading ? (
|
||||
<AlertTriangle className="animate-spin" />
|
||||
@ -39,7 +36,7 @@ export const DeleteDeploymentDialog = ({
|
||||
),
|
||||
}}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em">
|
||||
<p className="text-elements-high-em text-sm">
|
||||
Once deleted, the deployment will not be accessible.
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
} from '@/components/shared/ConfirmDialog';
|
||||
|
||||
interface DeleteDomainDialogProps extends ConfirmDialogProps {
|
||||
projectName: string;
|
||||
@ -23,9 +23,9 @@ export const DeleteDomainDialog = ({
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, delete domain"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
confirmButtonProps={{ variant: 'destructive' }}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em">
|
||||
<p className="text-elements-high-em text-sm">
|
||||
Once deleted, the project{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{projectName}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
} from '@/components/shared/ConfirmDialog';
|
||||
|
||||
interface DeleteVariableDialogProps extends ConfirmDialogProps {
|
||||
variableKey: string;
|
||||
@ -21,9 +21,9 @@ export const DeleteVariableDialog = ({
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, confirm delete"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
confirmButtonProps={{ variant: 'destructive' }}
|
||||
>
|
||||
<p className="text-sm text-elements-mid-em">
|
||||
<p className="text-elements-mid-em text-sm">
|
||||
Are you sure you want to delete the variable{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{variableKey}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
} from '@/components/shared/ConfirmDialog';
|
||||
|
||||
interface DeleteWebhookDialogProps extends ConfirmDialogProps {
|
||||
webhookUrl: string;
|
||||
@ -21,9 +21,9 @@ export const DeleteWebhookDialog = ({
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, confirm delete"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
confirmButtonProps={{ variant: 'destructive' }}
|
||||
>
|
||||
<p className="text-sm text-elements-mid-em">
|
||||
<p className="text-elements-mid-em text-sm">
|
||||
Are you sure you want to delete{' '}
|
||||
<span className="text-sm font-mono text-elements-high-em px-0.5">
|
||||
{webhookUrl}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
} from '@/components/shared/ConfirmDialog';
|
||||
|
||||
interface DisconnectRepositoryDialogProps extends ConfirmDialogProps {}
|
||||
|
||||
@ -18,9 +18,9 @@ export const DisconnectRepositoryDialog = ({
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, confirm disconnect"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
confirmButtonProps={{ variant: 'destructive' }}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em">
|
||||
<p className="text-elements-high-em text-sm">
|
||||
Any data tied to your Git project may become misconfigured. Are you sure
|
||||
you want to continue?
|
||||
</p>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
} from '@/components/shared/ConfirmDialog';
|
||||
|
||||
import { formatAddress } from 'utils/format';
|
||||
import { formatAddress } from '@/utils/format';
|
||||
|
||||
interface RemoveMemberDialogProps extends ConfirmDialogProps {
|
||||
memberName: string;
|
||||
@ -27,9 +27,9 @@ export const RemoveMemberDialog = ({
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, remove member"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
confirmButtonProps={{ variant: 'destructive' }}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em">
|
||||
<p className="text-elements-high-em text-sm">
|
||||
Once removed, {formatAddress(memberName)} ({formatAddress(ethAddress)}@
|
||||
{emailDomain}) will not be able to access this project.
|
||||
</p>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
} from '@/components/shared/ConfirmDialog';
|
||||
|
||||
interface TransferProjectDialogProps extends ConfirmDialogProps {
|
||||
projectName: string;
|
||||
|
@ -1,28 +1,24 @@
|
||||
import { WavyBorder } from '@/components/shared/WavyBorder';
|
||||
import { Button } from '@/components/ui';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import {
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
ComponentPropsWithoutRef,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { getInitials } from '@/utils/geInitials';
|
||||
import { relativeTimeMs } from '@/utils/time';
|
||||
import { Project } from 'gql-client';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
GitBranch,
|
||||
AlertTriangle,
|
||||
MoreHorizontal,
|
||||
Clock,
|
||||
} from 'lucide-react';
|
||||
import { Tooltip } from 'components/shared/Tooltip';
|
||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { AlertTriangle, Clock, GitBranch, MoreHorizontal } from 'lucide-react';
|
||||
import { ComponentPropsWithoutRef, MouseEvent, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
|
||||
|
||||
export interface ProjectCardProps
|
||||
@ -66,6 +62,25 @@ export const ProjectCard = ({
|
||||
[project.id, navigate],
|
||||
);
|
||||
|
||||
const handleDeleteClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// TODO: Add delete functionality
|
||||
navigate(`projects/${project.id}/settings`);
|
||||
},
|
||||
[project.id, navigate],
|
||||
);
|
||||
|
||||
const handleSettingsClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
navigate(`projects/${project.id}/settings`);
|
||||
},
|
||||
[project.id, navigate],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
@ -75,50 +90,51 @@ export const ProjectCard = ({
|
||||
{/* Upper content */}
|
||||
<div className={theme.upperContent()}>
|
||||
{/* Icon container */}
|
||||
<Avatar
|
||||
size={48}
|
||||
imageSrc={project.icon}
|
||||
initials={getInitials(project.name)}
|
||||
/>
|
||||
<Avatar className="w-12 h-12">
|
||||
<AvatarImage src={project.icon} alt={project.name} />
|
||||
<AvatarFallback>{getInitials(project.name)}</AvatarFallback>
|
||||
</Avatar>
|
||||
{/* Title and website */}
|
||||
<div className={theme.content()}>
|
||||
<Tooltip content={project.name}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<p className={theme.title()}>{project.name}</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{project.name}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<p className={theme.description()}>
|
||||
{project.deployments[0]?.applicationDeploymentRecordData?.url ?? 'No domain'}
|
||||
{project.deployments[0]?.applicationDeploymentRecordData?.url ??
|
||||
'No domain'}
|
||||
</p>
|
||||
</div>
|
||||
{/* Icons */}
|
||||
<div className={theme.icons()}>
|
||||
{hasError && <AlertTriangle className="text-error" />}
|
||||
<Menu placement="bottom-end">
|
||||
<MenuHandler>
|
||||
<Button
|
||||
shape="default"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
iconOnly
|
||||
onClick={handleOptionsClick}
|
||||
>
|
||||
<MoreHorizontal />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||
<Button variant="ghost">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</MenuHandler>
|
||||
<MenuList className="dark:bg-overlay3 dark:shadow-background dark:border-none">
|
||||
<MenuItem
|
||||
onClick={navigateToSettingsOnClick}
|
||||
className="text-foreground"
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DropdownMenuItem onClick={handleSettingsClick}>
|
||||
Project settings
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={handleDeleteClick}
|
||||
className="text-error"
|
||||
onClick={navigateToSettingsOnClick}
|
||||
>
|
||||
Delete project
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
{/* Wave */}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCombobox } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useDebounceValue } from 'usehooks-ts';
|
||||
|
||||
import SearchBar from 'components/SearchBar';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { cn } from 'utils/classnames';
|
||||
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
|
||||
import SearchBar from '@/components/SearchBar';
|
||||
import { useGQLClient } from '@/context/GQLClientContext';
|
||||
import { cn } from '@/utils/classnames';
|
||||
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
|
||||
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
|
||||
|
||||
interface ProjectSearchBarProps {
|
||||
onChange?: (data: Project) => void;
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ProjectSearchBarItem } from '@/components/projects/ProjectSearchBar/ProjectSearchBarItem';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useGQLClient } from '@/context/GQLClientContext';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { X, Search } from 'lucide-react';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { Project } from 'gql-client';
|
||||
import { useDebounceValue } from 'usehooks-ts';
|
||||
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
|
||||
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useCombobox } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import { Search, X } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDebounceValue } from 'usehooks-ts';
|
||||
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
|
||||
|
||||
interface ProjectSearchBarDialogProps extends Dialog.DialogProps {
|
||||
open?: boolean;
|
||||
@ -26,6 +26,7 @@ export const ProjectSearchBarDialog = ({
|
||||
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
|
||||
const client = useGQLClient();
|
||||
const navigate = useNavigate();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const {
|
||||
getInputProps,
|
||||
@ -75,22 +76,32 @@ export const ProjectSearchBarDialog = ({
|
||||
return (
|
||||
<Dialog.Root {...props}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="bg-base-bg fixed inset-0 md:hidden overflow-y-auto" />
|
||||
<Dialog.Overlay className="bg-base-bg md:hidden fixed inset-0 overflow-y-auto" />
|
||||
<Dialog.Content>
|
||||
<div className="h-full flex flex-col fixed top-0 inset-0">
|
||||
<div className="fixed inset-0 top-0 flex flex-col h-full">
|
||||
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
|
||||
<div className="relative flex-1">
|
||||
<Search className="left-2 top-1/2 text-muted-foreground absolute w-4 h-4 -translate-y-1/2" />
|
||||
<Input
|
||||
{...getInputProps({}, { suppressRefError: true })}
|
||||
leftIcon={<Search />}
|
||||
{...getInputProps(
|
||||
{ ref: inputRef },
|
||||
{ suppressRefError: true },
|
||||
)}
|
||||
className="pl-8"
|
||||
placeholder="Search"
|
||||
appearance="borderless"
|
||||
autoFocus
|
||||
type="text"
|
||||
/>
|
||||
<Button iconOnly variant="ghost" onClick={handleClose}>
|
||||
<X size={16} />
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleClose}
|
||||
type="button"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div
|
||||
className="flex flex-col gap-1 px-2 py-2"
|
||||
{...getMenuProps(
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { cn } from '@/utils/classnames';
|
||||
import { Search } from 'lucide-react';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
interface ProjectSearchBarEmptyProps extends ComponentPropsWithoutRef<'div'> {}
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Avatar } from '@/components/shared/Avatar';
|
||||
import { OmitCommon } from '@/types/common';
|
||||
import { cn } from '@/utils/classnames';
|
||||
import { getInitials } from '@/utils/geInitials';
|
||||
import { Overwrite, UseComboboxGetItemPropsReturnValue } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import { ComponentPropsWithoutRef, forwardRef } from 'react';
|
||||
import { OmitCommon } from 'types/common';
|
||||
import { cn } from 'utils/classnames';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
|
||||
/**
|
||||
* Represents a type that merges ComponentPropsWithoutRef<'li'> with certain exclusions.
|
||||
|
@ -1,10 +1,20 @@
|
||||
import { Modal, ModalContent } from 'components/shared';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Modal } from '@/components/shared/Modal';
|
||||
import { ModalContent } from '@/components/shared/Modal/ModalContent';
|
||||
// import { Modal } from '@mui/material';
|
||||
import {
|
||||
VITE_LACONICD_CHAIN_ID,
|
||||
VITE_WALLET_IFRAME_URL,
|
||||
} from 'utils/constants';
|
||||
} from '@/utils/constants';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* ApproveTransactionModal component that displays a modal with an iframe to approve transactions.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {function} props.setAccount - Callback function to set the account.
|
||||
* @param {function} props.setIsDataReceived - Callback function to set whether the data is received.
|
||||
* @param {boolean} props.isVisible - Determines whether the modal is visible.
|
||||
* @returns {JSX.Element} - The ApproveTransactionModal component.
|
||||
*/
|
||||
const ApproveTransactionModal = ({
|
||||
setAccount,
|
||||
setIsDataReceived,
|
||||
@ -22,7 +32,9 @@ const ApproveTransactionModal = ({
|
||||
setIsDataReceived(true);
|
||||
|
||||
if (event.data.data.length === 0) {
|
||||
console.error(`Accounts not present for chainId: ${VITE_LACONICD_CHAIN_ID}`);
|
||||
console.error(
|
||||
`Accounts not present for chainId: ${VITE_LACONICD_CHAIN_ID}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -41,6 +53,11 @@ const ApproveTransactionModal = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Gets the data from the wallet.
|
||||
* @function getDataFromWallet
|
||||
* @returns {void}
|
||||
*/
|
||||
const getDataFromWallet = useCallback(() => {
|
||||
const iframe = document.getElementById('walletIframe') as HTMLIFrameElement;
|
||||
|
||||
@ -59,7 +76,7 @@ const ApproveTransactionModal = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal open={isVisible} onClose={() => {}}>
|
||||
<Modal open={isVisible}>
|
||||
<ModalContent className="fixed left-1/2 top-1/2 w-[90%] max-w-[1200px] -translate-x-1/2 -translate-y-1/2 overflow-auto rounded-lg border bg-background shadow-lg">
|
||||
<iframe
|
||||
onLoad={getDataFromWallet}
|
||||
|
@ -2,12 +2,20 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
import { Modal } from '@mui/material';
|
||||
|
||||
import { VITE_WALLET_IFRAME_URL } from 'utils/constants';
|
||||
import useCheckBalance from '../../../hooks/useCheckBalance';
|
||||
import { VITE_WALLET_IFRAME_URL } from '@/utils/constants';
|
||||
import useCheckBalance from '@/hooks/useCheckBalance';
|
||||
|
||||
const CHECK_BALANCE_INTERVAL = 5000;
|
||||
const IFRAME_ID = 'checkBalanceIframe';
|
||||
|
||||
/**
|
||||
* CheckBalanceIframe component that checks the balance using an iframe.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {function} props.onBalanceChange - Callback function to be called when the balance changes.
|
||||
* @param {boolean} props.isPollingEnabled - Determines whether to poll the balance periodically.
|
||||
* @param {string} props.amount - The amount to check against the balance.
|
||||
* @returns {JSX.Element} - The CheckBalanceIframe component.
|
||||
*/
|
||||
const CheckBalanceIframe = ({
|
||||
onBalanceChange,
|
||||
isPollingEnabled,
|
||||
@ -24,6 +32,9 @@ const CheckBalanceIframe = ({
|
||||
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
/**
|
||||
* useEffect hook that calls checkBalance when the component is loaded or the amount changes.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isLoaded) {
|
||||
return;
|
||||
@ -31,6 +42,10 @@ const CheckBalanceIframe = ({
|
||||
checkBalance();
|
||||
}, [amount, checkBalance, isLoaded]);
|
||||
|
||||
/**
|
||||
* useEffect hook that sets up an interval to poll the balance if polling is enabled.
|
||||
* Clears the interval when the component unmounts, balance is sufficient, or polling is disabled.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isPollingEnabled || !isLoaded || isBalanceSufficient) {
|
||||
return;
|
||||
@ -45,9 +60,12 @@ const CheckBalanceIframe = ({
|
||||
};
|
||||
}, [isBalanceSufficient, isPollingEnabled, checkBalance, isLoaded]);
|
||||
|
||||
/**
|
||||
* useEffect hook that calls the onBalanceChange callback when the isBalanceSufficient state changes.
|
||||
*/
|
||||
useEffect(() => {
|
||||
onBalanceChange(isBalanceSufficient);
|
||||
}, [isBalanceSufficient]);
|
||||
}, [isBalanceSufficient, onBalanceChange]);
|
||||
|
||||
return (
|
||||
<Modal open={false} disableEscapeKeyDown keepMounted>
|
||||
|
@ -5,23 +5,29 @@ import {
|
||||
Deployer,
|
||||
} from 'gql-client';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, FieldValues, FormProvider, useForm } from 'react-hook-form';
|
||||
import {
|
||||
Controller,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useForm,
|
||||
} from 'react-hook-form';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
|
||||
import { FormControl, FormHelperText, MenuItem, Select } from '@mui/material';
|
||||
import { useToast } from '@/components/shared/Toast';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { IconButton } from '@/components/ui/extended/button-w-icons';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import EnvironmentVariablesForm from '@/pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
|
||||
import { EnvironmentVariablesFormValues } from '@/types';
|
||||
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { ArrowRightCircle, Loader2 } from 'lucide-react';
|
||||
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
|
||||
import { EnvironmentVariablesFormValues } from 'types/types';
|
||||
import {
|
||||
VITE_LACONICD_CHAIN_ID,
|
||||
VITE_WALLET_IFRAME_URL,
|
||||
} from 'utils/constants';
|
||||
} from '@/utils/constants';
|
||||
import { FormControl, FormHelperText, MenuItem, Select } from '@mui/material';
|
||||
import { ArrowRightCircle, Loader2 } from 'lucide-react';
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
import { Button } from '../../shared/Button';
|
||||
import { Heading } from '../../shared/Heading';
|
||||
import ApproveTransactionModal from './ApproveTransactionModal';
|
||||
import CheckBalanceIframe from './CheckBalanceIframe';
|
||||
@ -39,6 +45,10 @@ type ConfigureFormValues = ConfigureDeploymentFormValues &
|
||||
const DEFAULT_MAX_PRICE = '10000';
|
||||
const TX_APPROVAL_TIMEOUT_MS = 60000;
|
||||
|
||||
/**
|
||||
* Configure component that allows users to configure their deployment.
|
||||
* @returns {JSX.Element} - The Configure component.
|
||||
*/
|
||||
const Configure = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [deployers, setDeployers] = useState<Deployer[]>([]);
|
||||
@ -106,6 +116,16 @@ const Configure = () => {
|
||||
selectedNumProviders,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Creates a project using the GQLClient.
|
||||
* @async
|
||||
* @function createProject
|
||||
* @param {FieldValues} data - The form data.
|
||||
* @param {AddEnvironmentVariableInput[]} envVariables - The environment variables.
|
||||
* @param {string} senderAddress - The sender address.
|
||||
* @param {string} txHash - The transaction hash.
|
||||
* @returns {Promise<string>} - The project ID.
|
||||
*/
|
||||
const createProject = async (
|
||||
data: FieldValues,
|
||||
envVariables: AddEnvironmentVariableInput[],
|
||||
@ -185,6 +205,15 @@ const Configure = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies a transaction using the GQLClient.
|
||||
* @async
|
||||
* @function verifyTx
|
||||
* @param {string} senderAddress - The sender address.
|
||||
* @param {string} txHash - The transaction hash.
|
||||
* @param {string} amount - The amount.
|
||||
* @returns {Promise<boolean>} - True if the transaction is valid, false otherwise.
|
||||
*/
|
||||
const verifyTx = async (
|
||||
senderAddress: string,
|
||||
txHash: string,
|
||||
@ -199,6 +228,13 @@ const Configure = () => {
|
||||
return isValid;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the form submission.
|
||||
* @async
|
||||
* @function handleFormSubmit
|
||||
* @param {FieldValues} createFormData - The form data.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const handleFormSubmit = useCallback(
|
||||
async (createFormData: FieldValues) => {
|
||||
try {
|
||||
@ -315,11 +351,23 @@ const Configure = () => {
|
||||
[client, createProject, dismiss, toast, amountToBePaid],
|
||||
);
|
||||
|
||||
/**
|
||||
* Fetches the deployers using the GQLClient.
|
||||
* @async
|
||||
* @function fetchDeployers
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const fetchDeployers = useCallback(async () => {
|
||||
const res = await client.getDeployers();
|
||||
setDeployers(res.deployers);
|
||||
}, [client]);
|
||||
|
||||
/**
|
||||
* Handles the deployer change.
|
||||
* @function onDeployerChange
|
||||
* @param {string} selectedLrn - The selected LRN.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onDeployerChange = useCallback(
|
||||
(selectedLrn: string) => {
|
||||
const deployer = deployers.find((d) => d.deployerLrn === selectedLrn);
|
||||
@ -328,6 +376,14 @@ const Configure = () => {
|
||||
[deployers],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles the cosmos send tokens.
|
||||
* @async
|
||||
* @function cosmosSendTokensHandler
|
||||
* @param {string} selectedAccount - The selected account.
|
||||
* @param {string} amount - The amount.
|
||||
* @returns {Promise<string>} - The transaction hash.
|
||||
*/
|
||||
const cosmosSendTokensHandler = useCallback(
|
||||
async (selectedAccount: string, amount: string) => {
|
||||
if (!selectedAccount) {
|
||||
@ -399,6 +455,15 @@ const Configure = () => {
|
||||
[client, dismiss, toast],
|
||||
);
|
||||
|
||||
/**
|
||||
* Requests a transaction.
|
||||
* @async
|
||||
* @function requestTx
|
||||
* @param {string} sender - The sender.
|
||||
* @param {string} recipient - The recipient.
|
||||
* @param {string} amount - The amount.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const requestTx = async (
|
||||
sender: string,
|
||||
recipient: string,
|
||||
@ -435,6 +500,11 @@ const Configure = () => {
|
||||
}
|
||||
}, [isBalanceSufficient]);
|
||||
|
||||
/**
|
||||
* Handles the configure deployment.
|
||||
* @function handleConfigureDeployment
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleConfigureDeployment = useCallback(() => {
|
||||
methods.handleSubmit(handleFormSubmit)();
|
||||
}, [handleFormSubmit, methods]);
|
||||
@ -448,7 +518,7 @@ const Configure = () => {
|
||||
</Heading>
|
||||
<Heading
|
||||
as="h5"
|
||||
className="text-sm font-sans text-elements-low-em dark:text-foreground-secondaryu"
|
||||
className="text-elements-low-em dark:text-foreground-secondaryu font-sans text-sm"
|
||||
>
|
||||
The app can be deployed by setting the deployer LRN for a single
|
||||
deployment or by creating a deployer auction for multiple
|
||||
@ -457,7 +527,7 @@ const Configure = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6 lg:gap-8 w-full">
|
||||
<div className="lg:gap-8 flex flex-col w-full gap-6">
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={methods.handleSubmit(handleFormSubmit)}>
|
||||
<div className="flex flex-col justify-start gap-4 mb-6">
|
||||
@ -490,7 +560,7 @@ const Configure = () => {
|
||||
<div className="flex flex-col justify-start gap-4 mb-6">
|
||||
<Heading
|
||||
as="h5"
|
||||
className="text-sm font-sans text-elements-low-em dark:text-foreground-secondary"
|
||||
className="text-elements-low-em dark:text-foreground-secondary font-sans text-sm"
|
||||
>
|
||||
The app will be deployed by the configured deployer
|
||||
</Heading>
|
||||
@ -500,14 +570,16 @@ const Configure = () => {
|
||||
rules={{ required: true }}
|
||||
render={({ field: { value, onChange }, fieldState }) => (
|
||||
<FormControl fullWidth error={Boolean(fieldState.error)}>
|
||||
<span className="text-sm dark:text-foreground text-elements-high-em dark:text-foreground mb-4">
|
||||
<span className="dark:text-foreground text-elements-high-em mb-4 text-sm">
|
||||
Select deployer LRN
|
||||
</span>
|
||||
<Select
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
onDeployerChange(event.target.value);
|
||||
onChange((event.target as HTMLInputElement).value);
|
||||
onDeployerChange(
|
||||
(event.target as HTMLInputElement).value,
|
||||
);
|
||||
}}
|
||||
displayEmpty
|
||||
size="small"
|
||||
@ -538,12 +610,12 @@ const Configure = () => {
|
||||
<div className="flex flex-col justify-start gap-4 mb-6">
|
||||
<Heading
|
||||
as="h5"
|
||||
className="text-sm font-sans text-elements-low-em dark:text-foreground-secondary"
|
||||
className="text-elements-low-em dark:text-foreground-secondary font-sans text-sm"
|
||||
>
|
||||
Set the number of deployers and maximum price for each
|
||||
deployment
|
||||
</Heading>
|
||||
<span className="text-sm text-elements-high-em dark:text-foreground">
|
||||
<span className="text-elements-high-em dark:text-foreground text-sm">
|
||||
Number of Deployers
|
||||
</span>
|
||||
<Controller
|
||||
@ -553,15 +625,16 @@ const Configure = () => {
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e)}
|
||||
min={1}
|
||||
value={value ?? ''}
|
||||
onChange={(e) =>
|
||||
onChange((e.target as HTMLInputElement).value)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start gap-4 mb-6">
|
||||
<span className="text-sm text-elements-high-em dark:text-foreground">
|
||||
<span className="text-elements-high-em dark:text-foreground text-sm">
|
||||
Maximum Price (alnt)
|
||||
</span>
|
||||
<Controller
|
||||
@ -569,41 +642,45 @@ const Configure = () => {
|
||||
control={methods.control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input type="number" value={value} onChange={onChange} min={1} />
|
||||
<Input
|
||||
type="number"
|
||||
value={value ?? ''}
|
||||
onChange={(e) =>
|
||||
onChange((e.target as HTMLInputElement).value)
|
||||
}
|
||||
// min={1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Heading as="h4" className="md:text-lg font-medium mb-3">
|
||||
<Heading as="h4" className="md:text-lg mb-3 font-medium">
|
||||
Environment Variables
|
||||
</Heading>
|
||||
<div className="p-4 bg-slate-100 dark:bg-overlay3 rounded-lg mb-6">
|
||||
<div className="bg-slate-100 dark:bg-overlay3 p-4 mb-6 rounded-lg">
|
||||
<EnvironmentVariablesForm />
|
||||
</div>
|
||||
|
||||
{selectedOption === 'LRN' && !selectedDeployer?.minimumPayment ? (
|
||||
<div>
|
||||
<Button
|
||||
<IconButton
|
||||
{...buttonSize}
|
||||
type="submit"
|
||||
title="Deploy"
|
||||
disabled={isLoading || !selectedDeployer}
|
||||
rightIcon={
|
||||
isLoading ? (
|
||||
<Loader2 className="ml-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<ArrowRightCircle className="ml-2 h-4 w-4" />
|
||||
)
|
||||
}
|
||||
>
|
||||
{isLoading ? 'Deploying' : 'Deploy'}
|
||||
</Button>
|
||||
isLoading={isLoading}
|
||||
loadingText="Deploying"
|
||||
rightIcon={<ArrowRightCircle />}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
<IconButton
|
||||
{...buttonSize}
|
||||
type="submit"
|
||||
title="Configure"
|
||||
disabled={
|
||||
isLoading ||
|
||||
isPaymentLoading ||
|
||||
@ -612,33 +689,16 @@ const Configure = () => {
|
||||
amountToBePaid === '' ||
|
||||
selectedNumProviders === ''
|
||||
}
|
||||
rightIcon={
|
||||
isLoading || isPaymentLoading ? (
|
||||
<Loader2 className="ml-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<ArrowRightCircle className="ml-2 h-4 w-4" />
|
||||
)
|
||||
}
|
||||
isLoading={isLoading || isPaymentLoading}
|
||||
loadingText="Configuring"
|
||||
rightIcon={<ArrowRightCircle />}
|
||||
onClick={handleConfigureDeployment}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
Configuring...
|
||||
<Loader2 className="ml-2 h-4 w-4 animate-spin" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Configure
|
||||
<ArrowRightCircle className="ml-2 h-4 w-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
/>
|
||||
{isAccountsDataReceived && isBalanceSufficient !== undefined ? (
|
||||
!selectedAccount || !isBalanceSufficient ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
{...buttonSize}
|
||||
shape="default"
|
||||
onClick={(e: any) => {
|
||||
e.preventDefault();
|
||||
setBalanceMessage('Waiting for payment');
|
||||
@ -651,7 +711,7 @@ const Configure = () => {
|
||||
>
|
||||
Buy prepaid service
|
||||
</Button>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
<p className="dark:text-gray-300 text-gray-700">
|
||||
{balanceMessage !== undefined ? (
|
||||
<div className="flex items-center gap-2 text-white">
|
||||
<Loader2 className="animate-spin w-5 h-5" />
|
||||
|
@ -2,14 +2,13 @@ import React from 'react';
|
||||
import OauthPopup from 'react-oauth-popup';
|
||||
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
import { Button } from '../../shared/Button';
|
||||
|
||||
import { Github, GitBranch, MoreHorizontal } from 'lucide-react';
|
||||
import { VITE_GITHUB_CLIENT_ID } from 'utils/constants';
|
||||
import { IconButton } from '@/components/ui/extended/button-w-icons';
|
||||
import { GitBranch, Github, MoreHorizontal } from 'lucide-react';
|
||||
import { VITE_GITHUB_CLIENT_ID } from '../../../utils/constants';
|
||||
import { Heading } from '../../shared/Heading';
|
||||
import { IconWithFrame } from '../../shared/IconWithFrame';
|
||||
import { useToast } from '../../shared/Toast';
|
||||
import { MockConnectGitCard } from './MockConnectGitCard';
|
||||
|
||||
const SCOPES = 'public_repo user';
|
||||
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`;
|
||||
@ -18,6 +17,12 @@ interface ConnectAccountInterface {
|
||||
onAuth: (token: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConnectAccount component that allows users to connect to their Git account.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {function} props.onAuth - Callback function to be called when the user is authenticated.
|
||||
* @returns {JSX.Element} - The ConnectAccount component.
|
||||
*/
|
||||
const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
||||
onAuth: onToken,
|
||||
}: ConnectAccountInterface) => {
|
||||
@ -25,6 +30,13 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
||||
|
||||
const { toast, dismiss } = useToast();
|
||||
|
||||
/**
|
||||
* Handles the code received from the OAuth popup.
|
||||
* @async
|
||||
* @function handleCode
|
||||
* @param {string} code - The code received from the OAuth popup.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const handleCode = async (code: string) => {
|
||||
// Pass code to backend and get access token
|
||||
const {
|
||||
@ -41,10 +53,10 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
||||
|
||||
// TODO: Use correct height
|
||||
return (
|
||||
<div className="dark:bg-overlay bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl">
|
||||
<div className="dark:bg-overlay gap-7 rounded-2xl flex flex-col items-center justify-center h-full p-4 text-sm text-center bg-gray-100">
|
||||
<div className="flex flex-col items-center max-w-[420px]">
|
||||
{/** Icons */}
|
||||
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex mb-7">
|
||||
<div className="w-52 mb-7 inline-flex items-center justify-center h-16 gap-4">
|
||||
<IconWithFrame icon={<GitBranch />} hasHighlight={false} />
|
||||
<MoreHorizontal className="items-center gap-1.5 flex" />
|
||||
<IconWithFrame
|
||||
@ -55,16 +67,16 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
||||
</div>
|
||||
{/** Text */}
|
||||
<div className="flex flex-col gap-1.5 mb-6">
|
||||
<Heading className="text-xl font-medium dark:text-foreground">
|
||||
<Heading className="dark:text-foreground text-xl font-medium">
|
||||
Connect to your Git account
|
||||
</Heading>
|
||||
<p className="text-center text-elements-mid-em dark:text-foreground-secondary">
|
||||
<p className="text-elements-mid-em dark:text-foreground-secondary text-center">
|
||||
Once connected, you can import a repository from your account or
|
||||
start with one of our templates.
|
||||
</p>
|
||||
</div>
|
||||
{/** CTA Buttons */}
|
||||
<div className="flex flex-col w-full sm:w-auto sm:flex-row gap-2 sm:gap-3">
|
||||
<div className="sm:w-auto sm:flex-row sm:gap-3 flex flex-col w-full gap-2">
|
||||
<OauthPopup
|
||||
url={GITHUB_OAUTH_URL}
|
||||
onCode={handleCode}
|
||||
@ -73,27 +85,27 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
||||
width={1000}
|
||||
height={1000}
|
||||
>
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
leftIcon={<Github />}
|
||||
variant="primary"
|
||||
<IconButton
|
||||
className="sm:w-auto w-full"
|
||||
leftIcon={<GitBranch />}
|
||||
variant="default"
|
||||
>
|
||||
Connect to GitHub
|
||||
</Button>
|
||||
</IconButton>
|
||||
</OauthPopup>
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
<IconButton
|
||||
className="sm:w-auto w-full"
|
||||
leftIcon={<Github />}
|
||||
variant="primary"
|
||||
variant="default"
|
||||
>
|
||||
Connect to GitTea
|
||||
</Button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TODO: Add ConnectAccountTabPanel */}
|
||||
<MockConnectGitCard />
|
||||
{/* <div className="rounded-l shadow p-2 flex-col justify-start items-start gap-2 inline-flex">
|
||||
{/* <MockConnectGitCard /> */}
|
||||
{/* <div className="inline-flex flex-col items-start justify-start gap-2 p-2 rounded-l shadow">
|
||||
<ConnectAccountTabPanel />
|
||||
</div> */}
|
||||
</div>
|
||||
|
@ -1,7 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Tabs } from 'components/shared/Tabs';
|
||||
import { Tabs } from '@/components/shared/Tabs';
|
||||
|
||||
/**
|
||||
* ConnectAccountTabPanel component that renders a tab panel for connecting accounts.
|
||||
* @returns {JSX.Element} - The ConnectAccountTabPanel component.
|
||||
*/
|
||||
const ConnectAccountTabPanel: React.FC = () => {
|
||||
return (
|
||||
<Tabs
|
||||
|
@ -1,16 +1,15 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { Deployment } from 'gql-client';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { DeployStep, DeployStatus } from './DeployStep';
|
||||
import { IconButton } from '@/components/ui';
|
||||
import { useGQLClient } from '@/context/GQLClientContext';
|
||||
import { AlertTriangle, Clock } from 'lucide-react';
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import { Heading } from '../../shared/Heading';
|
||||
import { Button } from '../../shared/Button';
|
||||
import { Clock, AlertTriangle } from 'lucide-react';
|
||||
import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
|
||||
import { Heading } from '../../shared/Heading';
|
||||
import { DeployStatus, DeployStep } from './DeployStep';
|
||||
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
|
||||
|
||||
type RequestState =
|
||||
@ -30,6 +29,10 @@ type Record = {
|
||||
logAvailable: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deploy component that displays the deployment status of a project.
|
||||
* @returns {JSX.Element} - The Deploy component.
|
||||
*/
|
||||
const Deploy = () => {
|
||||
const client = useGQLClient();
|
||||
|
||||
@ -49,6 +52,10 @@ const Deploy = () => {
|
||||
navigate(`/${orgSlug}/projects/create`);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Checks if the deployment has failed based on the record's last state.
|
||||
* @returns {boolean} - True if the deployment has failed, false otherwise.
|
||||
*/
|
||||
const isDeploymentFailed = useMemo(() => {
|
||||
if (!record) {
|
||||
return false;
|
||||
@ -62,6 +69,12 @@ const Deploy = () => {
|
||||
}
|
||||
}, [record]);
|
||||
|
||||
/**
|
||||
* Fetches the deployment records from the deployer API.
|
||||
* @async
|
||||
* @function fetchDeploymentRecords
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const fetchDeploymentRecords = useCallback(async () => {
|
||||
if (!deployment) {
|
||||
return;
|
||||
@ -79,6 +92,12 @@ const Deploy = () => {
|
||||
}
|
||||
}, [deployment]);
|
||||
|
||||
/**
|
||||
* Fetches the deployment information from the GQLClient.
|
||||
* @async
|
||||
* @function fetchDeployment
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const fetchDeployment = useCallback(async () => {
|
||||
if (!projectId) {
|
||||
return;
|
||||
@ -126,14 +145,14 @@ const Deploy = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
<IconButton
|
||||
onClick={handleOpen}
|
||||
leftIcon={<AlertTriangle />}
|
||||
aria-label="Cancel deployment"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
leftIcon={<AlertTriangle size={16} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</IconButton>
|
||||
<CancelDeploymentDialog
|
||||
handleCancel={handleOpen}
|
||||
open={open}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import { cn } from 'utils/classnames';
|
||||
import { cn } from '@/utils/classnames';
|
||||
import { Check, Clock, Loader2 } from 'lucide-react';
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
|
||||
enum DeployStatus {
|
||||
PROCESSING = 'progress',
|
||||
@ -9,6 +9,9 @@ enum DeployStatus {
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
/**
|
||||
* DeployStepsProps interface defines the props for the DeployStep component.
|
||||
*/
|
||||
interface DeployStepsProps {
|
||||
status: DeployStatus;
|
||||
title: string;
|
||||
@ -17,6 +20,11 @@ interface DeployStepsProps {
|
||||
processTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DeployStep component displays a single step in the deployment process.
|
||||
* @param {DeployStepsProps} props - The component props.
|
||||
* @returns {JSX.Element} - The DeployStep component.
|
||||
*/
|
||||
const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
|
||||
return (
|
||||
<div className="border-b border-border-separator">
|
||||
@ -64,10 +72,7 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-4.5 h-4.5 grid place-content-center">
|
||||
<Check
|
||||
className="text-elements-success"
|
||||
size={15}
|
||||
/>
|
||||
<Check className="text-elements-success" size={15} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -76,4 +81,4 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export { DeployStep, DeployStatus };
|
||||
export { DeployStatus, DeployStep };
|
||||
|
@ -1,12 +1,15 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { SegmentedControls } from 'components/shared/SegmentedControls';
|
||||
import { useState } from 'react';
|
||||
import { SegmentedControls } from '@/components/shared/SegmentedControls';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
|
||||
import { Github, GitBranch, LayoutDashboard } from 'lucide-react';
|
||||
import { relativeTimeISO } from 'utils/time';
|
||||
import templates from 'assets/templates';
|
||||
import templates from '@/assets/templates';
|
||||
import { relativeTimeISO } from '@/utils/time';
|
||||
import { GitBranch, Github, LayoutDashboard } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* MockConnectGitCard component that allows users to import a repository or start with a template.
|
||||
* @returns {JSX.Element} - The MockConnectGitCard component.
|
||||
*/
|
||||
export const MockConnectGitCard = () => {
|
||||
const [segmentedControlsValue, setSegmentedControlsValue] =
|
||||
useState<string>('import');
|
||||
@ -43,15 +46,20 @@ export const MockConnectGitCard = () => {
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Renders the content based on the selected segmented control value.
|
||||
* If the value is 'import', it renders a list of mock project cards.
|
||||
* If the value is 'template', it renders a grid of mock template cards.
|
||||
*/
|
||||
const renderContent = useMemo(() => {
|
||||
if (segmentedControlsValue === 'import') {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 relative z-0">
|
||||
<div className="relative z-0 flex flex-col gap-2">
|
||||
{IMPORT_CONTENT.map((repo, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<MockProjectCard {...repo} />
|
||||
{index !== IMPORT_CONTENT.length - 1 && (
|
||||
<div className="border-b border-base-border" />
|
||||
<div className="border-base-border border-b" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
@ -59,7 +67,7 @@ export const MockConnectGitCard = () => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 relative z-0">
|
||||
<div className="lg:grid-cols-2 relative z-0 grid grid-cols-1">
|
||||
{templates.map((template, index) => (
|
||||
<MockTemplateCard key={index} {...template} />
|
||||
))}
|
||||
@ -80,11 +88,19 @@ export const MockConnectGitCard = () => {
|
||||
{renderContent}
|
||||
|
||||
{/* Shade */}
|
||||
<div className="pointer-events-none z-99 absolute inset-0 rounded-2xl bg-gradient-to-t from-white dark:from-overlay to-transparent" />
|
||||
<div className="z-99 rounded-2xl bg-gradient-to-t from-white dark:from-overlay to-transparent absolute inset-0 pointer-events-none" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* MockProjectCard component that displays a mock project card.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.full_name - The full name of the repository.
|
||||
* @param {string} [props.updated_at] - The last updated date of the repository.
|
||||
* @param {string} [props.visibility] - The visibility of the repository.
|
||||
* @returns {JSX.Element} - The MockProjectCard component.
|
||||
*/
|
||||
const MockProjectCard = ({
|
||||
full_name,
|
||||
updated_at,
|
||||
@ -95,24 +111,24 @@ const MockProjectCard = ({
|
||||
visibility?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="group flex items-start sm:items-center gap-3 pl-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized dark:hover:bg-background relative">
|
||||
<div className="group sm:items-center rounded-xl hover:bg-base-bg-emphasized dark:hover:bg-background relative flex items-start gap-3 py-3 pl-3 cursor-pointer">
|
||||
{/* Icon container */}
|
||||
<div className="w-10 h-10 bg-base-bg dark:bg-background rounded-md justify-center items-center flex">
|
||||
<Github className="h-5 w-5" />
|
||||
<div className="bg-base-bg dark:bg-background flex items-center justify-center w-10 h-10 rounded-md">
|
||||
<Github className="w-5 h-5" />
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="flex flex-1 gap-3 flex-wrap">
|
||||
<div className="flex flex-wrap flex-1 gap-3">
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<p className="text-elements-high-em text-sm dark:text-foreground font-medium tracking-[-0.006em]">
|
||||
{full_name}
|
||||
</p>
|
||||
<p className="text-elements-low-em text-xs dark:text-foreground-secondary">
|
||||
<p className="text-elements-low-em dark:text-foreground-secondary text-xs">
|
||||
{updated_at && relativeTimeISO(updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
{visibility === 'private' && (
|
||||
<div className="bg-orange-50 border border-orange-200 px-2 py-1 flex items-center gap-1 rounded-lg text-xs text-orange-600 h-fit">
|
||||
<GitBranch className="h-4 w-4" />
|
||||
<div className="bg-orange-50 h-fit flex items-center gap-1 px-2 py-1 text-xs text-orange-600 border border-orange-200 rounded-lg">
|
||||
<GitBranch className="w-4 h-4" />
|
||||
Private
|
||||
</div>
|
||||
)}
|
||||
@ -121,11 +137,17 @@ const MockProjectCard = ({
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* MockTemplateCard component that displays a mock template card.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.name - The name of the template.
|
||||
* @returns {JSX.Element} - The MockTemplateCard component.
|
||||
*/
|
||||
const MockTemplateCard = ({ name }: { name: string }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
<p className="text-sm font-medium text-elements-primary">{name}</p>
|
||||
<LayoutDashboard className="w-4 h-4" />
|
||||
<p className="text-elements-primary text-sm font-medium">{name}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -3,17 +3,33 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Spinner } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { useToast } from '@/components/shared/Toast';
|
||||
import { Button } from '@/components/ui';
|
||||
import { useGQLClient } from '@/context/GQLClientContext';
|
||||
import { relativeTimeISO } from '@/utils/time';
|
||||
import { ArrowRight, Github, Lock } from 'lucide-react';
|
||||
import { relativeTimeISO } from 'utils/time';
|
||||
import { GitRepositoryDetails } from '../../../../types/types';
|
||||
|
||||
/**
|
||||
* Props for the ProjectRepoCard component.
|
||||
*/
|
||||
interface ProjectRepoCardProps {
|
||||
/**
|
||||
* The repository to display in the card.
|
||||
*/
|
||||
repository: GitRepositoryDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* ProjectRepoCard Component
|
||||
*
|
||||
* This component renders a card displaying information about a GitHub repository, including its name,
|
||||
* update date, and visibility. It allows users to select the repository to create a new project.
|
||||
* It uses `useNavigate` to navigate to the configuration page and `useToast` to display error messages.
|
||||
*
|
||||
* @param {ProjectRepoCardProps} props - The props for the ProjectRepoCard component.
|
||||
* @returns {JSX.Element} - The ProjectRepoCard component.
|
||||
*/
|
||||
export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
||||
repository,
|
||||
}) => {
|
||||
@ -41,15 +57,15 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex items-start sm:items-center gap-3 px-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized relative"
|
||||
className="group sm:items-center rounded-xl hover:bg-base-bg-emphasized relative flex items-start gap-3 px-3 py-3 cursor-pointer"
|
||||
onClick={createProject}
|
||||
>
|
||||
{/* Icon container */}
|
||||
<div className="w-10 h-10 bg-base-bg rounded-md justify-center items-center flex dark:bg-overlay">
|
||||
<div className="bg-base-bg dark:bg-overlay flex items-center justify-center w-10 h-10 rounded-md">
|
||||
<Github />
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="flex flex-1 gap-3 flex-wrap">
|
||||
<div className="flex flex-wrap flex-1 gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-elements-high-em dark:text-foreground text-sm font-medium tracking-[-0.006em]">
|
||||
{repository.full_name}
|
||||
@ -59,7 +75,7 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
{repository.visibility === 'private' && (
|
||||
<div className="bg-orange-50 border border-orange-200 px-2 py-1 flex items-center gap-1 rounded-lg text-xs text-orange-600 dark:text-error h-fit">
|
||||
<div className="bg-orange-50 dark:text-error h-fit flex items-center gap-1 px-2 py-1 text-xs text-orange-600 border border-orange-200 rounded-lg">
|
||||
<Lock />
|
||||
Private
|
||||
</div>
|
||||
@ -67,13 +83,12 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
||||
</div>
|
||||
{/* Right action */}
|
||||
{isLoading ? (
|
||||
<Spinner className="h-4 w-4 absolute right-3" />
|
||||
<Spinner className="right-3 absolute w-4 h-4" />
|
||||
) : (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
iconOnly
|
||||
className="sm:group-hover:flex hidden absolute right-3"
|
||||
className="sm:group-hover:flex right-3 absolute hidden"
|
||||
>
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
|
@ -1,22 +1,28 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import assert from 'assert';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
import { ProjectRepoCard } from 'components/projects/create/ProjectRepoCard';
|
||||
import { ProjectRepoCard } from '@/components/projects/create/ProjectRepoCard';
|
||||
import { Select, SelectOption } from '@/components/shared/Select';
|
||||
import { IconButton } from '@/components/ui/extended/button-w-icons';
|
||||
import { IconInput } from '@/components/ui/extended/input-w-icons';
|
||||
import { useOctokit } from '@/context/OctokitContext';
|
||||
import { Github, RotateCw, Search } from 'lucide-react';
|
||||
import { GitOrgDetails, GitRepositoryDetails } from '../../../../types/types';
|
||||
import {
|
||||
Github,
|
||||
RotateCw,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useOctokit } from 'context/OctokitContext';
|
||||
|
||||
const DEFAULT_SEARCHED_REPO = '';
|
||||
const REPOS_PER_PAGE = 5;
|
||||
|
||||
/**
|
||||
* RepositoryList Component
|
||||
*
|
||||
* This component renders a list of repositories fetched from GitHub based on the selected account
|
||||
* and search criteria. It allows users to select an organization or their personal account and search
|
||||
* for repositories within that account.
|
||||
* It uses `useOctokit` to fetch repositories and `ProjectRepoCard` to display each repository.
|
||||
*
|
||||
* @returns {JSX.Element} - The RepositoryList component.
|
||||
*/
|
||||
export const RepositoryList = () => {
|
||||
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
||||
const [selectedAccount, setSelectedAccount] = useState<SelectOption>();
|
||||
@ -128,7 +134,7 @@ export const RepositoryList = () => {
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
{/* Dropdown and search */}
|
||||
<div className="flex flex-col lg:flex-row gap-0 lg:gap-3 items-center">
|
||||
<div className="lg:flex-row lg:gap-3 flex flex-col items-center gap-0">
|
||||
<div className="lg:basis-1/3 w-full">
|
||||
<Select
|
||||
options={options}
|
||||
@ -139,8 +145,8 @@ export const RepositoryList = () => {
|
||||
onChange={(value) => setSelectedAccount(value as SelectOption)}
|
||||
/>
|
||||
</div>
|
||||
<div className="basis-2/3 flex w-full flex-grow">
|
||||
<Input
|
||||
<div className="basis-2/3 flex flex-grow w-full">
|
||||
<IconInput
|
||||
className="w-full"
|
||||
value={searchedRepo}
|
||||
placeholder="Search for repository"
|
||||
@ -164,18 +170,18 @@ export const RepositoryList = () => {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 p-6 flex flex-col gap-4 items-center justify-center">
|
||||
<div className="flex flex-col items-center justify-center gap-4 p-6 mt-4">
|
||||
<p className="text-elements-high-em dark:text-foreground font-sans">
|
||||
No repository found
|
||||
</p>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
<IconButton
|
||||
variant="outline"
|
||||
leftIcon={<RotateCw />}
|
||||
size="sm"
|
||||
onClick={handleResetFilters}
|
||||
>
|
||||
Reset filters
|
||||
</Button>
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Tag } from 'components/shared/Tag';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { Tag } from '@/components/shared/Tag';
|
||||
import { useToast } from '@/components/shared/Toast';
|
||||
import { Button } from '@/components/ui';
|
||||
import { cn } from '@/utils/classnames';
|
||||
import { GitBranch } from 'lucide-react';
|
||||
import React, { ComponentPropsWithoutRef, useCallback } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
export interface TemplateDetail {
|
||||
id: string;
|
||||
@ -60,11 +60,11 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="px-1 py-1 rounded-xl bg-base-bg border border-border-interactive/10 shadow-card-sm">
|
||||
<div className="rounded-xl bg-base-bg border-border-interactive/10 shadow-card-sm px-1 py-1 border">
|
||||
<GitBranch />
|
||||
</div>
|
||||
{/* Name */}
|
||||
<p className="flex-1 text-left text-sm tracking-tighter text-elements-high-em">
|
||||
<p className="text-elements-high-em flex-1 text-sm tracking-tighter text-left">
|
||||
{template.name}
|
||||
</p>
|
||||
{template?.isComingSoon ? (
|
||||
@ -73,10 +73,10 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
</Tag>
|
||||
) : (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
iconOnly
|
||||
className="group-hover:flex hidden absolute right-3"
|
||||
// iconOnly
|
||||
className="group-hover:flex right-3 absolute hidden"
|
||||
>
|
||||
<GitBranch />
|
||||
</Button>
|
||||
|
@ -10,42 +10,76 @@ import { useCallback, useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
Modal as Dialog,
|
||||
ModalContent as DialogContent,
|
||||
ModalFooter as DialogFooter,
|
||||
ModalTitle as DialogTitle,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from 'components/shared';
|
||||
} from '@/components/ui';
|
||||
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { GitBranch, Clock, CheckCircle, AlertTriangle } from 'lucide-react';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { OverflownText } from 'components/shared/OverflownText';
|
||||
import { Tag } from 'components/shared/Tag';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { Heading } from '@/components/shared/Heading';
|
||||
import { OverflownText } from '@/components/shared/OverflownText';
|
||||
import { Tag } from '@/components/shared/Tag';
|
||||
import { getInitials } from '@/utils/geInitials';
|
||||
import { relativeTimeMs } from '@/utils/time';
|
||||
import { AlertTriangle, CheckCircle, Clock, GitBranch } from 'lucide-react';
|
||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
import { DeploymentMenu } from './DeploymentMenu';
|
||||
|
||||
interface DeployDetailsCardProps {
|
||||
deployment: Deployment;
|
||||
currentDeployment: Deployment;
|
||||
onUpdate: () => Promise<void>;
|
||||
project: Project;
|
||||
prodBranchDomains: Domain[];
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<DeploymentStatus, 'attention' | 'positive' | 'negative' | 'neutral'> = {
|
||||
/**
|
||||
* Type definition for the status colors.
|
||||
*/
|
||||
const STATUS_COLORS: Record<
|
||||
DeploymentStatus,
|
||||
'attention' | 'positive' | 'negative' | 'neutral'
|
||||
> = {
|
||||
[DeploymentStatus.Building]: 'attention',
|
||||
[DeploymentStatus.Ready]: 'positive',
|
||||
[DeploymentStatus.Error]: 'negative',
|
||||
[DeploymentStatus.Deleting]: 'neutral',
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for the DeploymentDetailsCard component.
|
||||
*/
|
||||
interface DeployDetailsCardProps {
|
||||
/**
|
||||
* The deployment to display details for.
|
||||
*/
|
||||
deployment: Deployment;
|
||||
/**
|
||||
* The current deployment.
|
||||
*/
|
||||
currentDeployment: Deployment;
|
||||
/**
|
||||
* Callback function to update deployments.
|
||||
*/
|
||||
onUpdate: () => Promise<void>;
|
||||
/**
|
||||
* The project the deployment belongs to.
|
||||
*/
|
||||
project: Project;
|
||||
/**
|
||||
* The domains for the production branch of the project.
|
||||
*/
|
||||
prodBranchDomains: Domain[];
|
||||
}
|
||||
|
||||
/**
|
||||
* DeploymentDetailsCard Component
|
||||
*
|
||||
* This component renders a card displaying detailed information about a deployment, including its URL, status,
|
||||
* commit details, and deployment information. It also provides a menu for managing the deployment.
|
||||
* It uses `DeploymentMenu` for deployment management actions and `Tooltip` for displaying build logs.
|
||||
*
|
||||
* @param {DeployDetailsCardProps} props - The props for the DeploymentDetailsCard component.
|
||||
* @returns {JSX.Element} - The DeploymentDetailsCard component.
|
||||
*/
|
||||
const DeploymentDetailsCard = ({
|
||||
deployment,
|
||||
currentDeployment,
|
||||
@ -102,7 +136,11 @@ const DeploymentDetailsCard = ({
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className={className} style={{ cursor: 'pointer' }} onClick={fetchDeploymentLogs}>
|
||||
<div
|
||||
className={className}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={fetchDeploymentLogs}
|
||||
>
|
||||
<Tag
|
||||
type={STATUS_COLORS[deployment.status]}
|
||||
size="xs"
|
||||
@ -123,13 +161,13 @@ const DeploymentDetailsCard = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative">
|
||||
<div className="md:flex-row last:mb-0 last:pb-4 border-border-separator last:border-b-transparent relative flex flex-col gap-6 px-3 py-4 pb-6 mb-2 border-b">
|
||||
<div className="flex-1 flex justify-between w-full md:max-w-[30%] lg:max-w-[33%]">
|
||||
<div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full">
|
||||
{/* DEPLOYMENT URL */}
|
||||
{deployment.url && (
|
||||
<Heading
|
||||
className="text-sm font-medium text-elements-high-em tracking-tight"
|
||||
className="text-elements-high-em text-sm font-medium tracking-tight"
|
||||
as="h2"
|
||||
>
|
||||
<OverflownText content={deployment.url}>
|
||||
@ -138,32 +176,30 @@ const DeploymentDetailsCard = ({
|
||||
</Heading>
|
||||
)}
|
||||
{deployment.deployer.deployerLrn && (
|
||||
<span className="text-sm text-elements-low-em tracking-tight block mt-2">
|
||||
<span className="text-elements-low-em block mt-2 text-sm tracking-tight">
|
||||
Deployer LRN: {deployment.deployer.deployerLrn}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm text-elements-low-em tracking-tight block">
|
||||
<span className="text-elements-low-em block text-sm tracking-tight">
|
||||
{deployment.environment === Environment.Production
|
||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||
: 'Preview'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DEPLOYMENT STATUS */}
|
||||
{renderDeploymentStatus('w-[10%] max-w-[110px] hidden md:flex h-fit')}
|
||||
|
||||
{/* DEPLOYMENT COMMIT DETAILS */}
|
||||
<div className="flex w-full justify-between md:w-[25%]">
|
||||
<div className="text-sm max-w-[60%] md:max-w-full space-y-2 w-full text-elements-low-em">
|
||||
<span className="flex gap-1.5 items-center">
|
||||
<GitBranch className="h-4 w-4" />
|
||||
<GitBranch className="w-4 h-4" />
|
||||
<OverflownText content={deployment.branch}>
|
||||
{deployment.branch}
|
||||
</OverflownText>
|
||||
</span>
|
||||
<span className="flex w-full gap-2 items-center">
|
||||
<div className="h-4 w-4" />
|
||||
<span className="flex items-center w-full gap-2">
|
||||
<div className="w-4 h-4" />
|
||||
<OverflownText content={deployment.commitMessage}>
|
||||
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
{deployment.commitMessage}
|
||||
@ -172,29 +208,32 @@ const DeploymentDetailsCard = ({
|
||||
</div>
|
||||
{renderDeploymentStatus('flex md:hidden h-fit')}
|
||||
</div>
|
||||
|
||||
{/* DEPLOYMENT INFOs */}
|
||||
<div className="md:ml-auto w-full md:max-w-[312px] md:w-[30%] gap-1 2xl:gap-5 flex items-center justify-between text-elements-low-em text-sm">
|
||||
<div className="flex md:w-[70%] xl:items-center gap-2 flex-1 xl:flex-row md:flex-col">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Clock className="h-4 w-4" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
<OverflownText content={relativeTimeMs(deployment.createdAt) ?? ''}>
|
||||
{relativeTimeMs(deployment.createdAt)}
|
||||
</OverflownText>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div>
|
||||
<Avatar className="lg:size-5 2xl:size-6">
|
||||
<AvatarFallback>{getInitials(deployment.createdBy.name ?? '')}</AvatarFallback>
|
||||
<AvatarFallback>
|
||||
{getInitials(deployment.createdBy.name ?? '')}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<OverflownText content={formatAddress(deployment.createdBy?.name ?? '')}>
|
||||
<OverflownText
|
||||
content={formatAddress(deployment.createdBy?.name ?? '')}
|
||||
>
|
||||
{formatAddress(deployment.createdBy.name ?? '')}
|
||||
</OverflownText>
|
||||
</div>
|
||||
</div>
|
||||
<DeploymentMenu
|
||||
className="ml-auto md:static absolute top-4 right-0"
|
||||
className="md:static top-4 absolute right-0 ml-auto"
|
||||
deployment={deployment}
|
||||
currentDeployment={currentDeployment}
|
||||
onUpdate={onUpdate}
|
||||
@ -202,6 +241,7 @@ const DeploymentDetailsCard = ({
|
||||
prodBranchDomains={prodBranchDomains}
|
||||
/>
|
||||
</div>
|
||||
{/* @ts-ignore */}
|
||||
<Dialog open={openDialog} onClose={handleCloseDialog}>
|
||||
<DialogTitle>Deployment logs</DialogTitle>
|
||||
<DialogContent className="bg-[rgba(0,0,0,0.9)] p-8 rounded-lg mx-2 text-gray-400 text-sm">
|
||||
|
@ -1,16 +1,12 @@
|
||||
import { Deployment } from 'gql-client';
|
||||
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { Avatar } from '@/components/shared/Avatar';
|
||||
import { OverflownText } from '@/components/shared/OverflownText';
|
||||
import { Tag, TagProps } from '@/components/shared/Tag';
|
||||
import { getInitials } from '@/utils/geInitials';
|
||||
import { relativeTimeMs } from '@/utils/time';
|
||||
import { Clock, GitBranch, GitCommit } from 'lucide-react';
|
||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||
import {
|
||||
GitBranch,
|
||||
Clock,
|
||||
GitCommit,
|
||||
} from 'lucide-react';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { OverflownText } from 'components/shared/OverflownText';
|
||||
import { Tag, TagProps } from 'components/shared/Tag';
|
||||
|
||||
interface DeploymentDialogBodyCardProps {
|
||||
deployment: Deployment;
|
||||
|
@ -7,11 +7,12 @@ import {
|
||||
MenuList,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
|
||||
import { DeleteDeploymentDialog } from 'components/projects/Dialog/DeleteDeploymentDialog';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { ChangeStateToProductionDialog } from '@/components/projects/Dialog/ChangeStateToProductionDialog';
|
||||
import { DeleteDeploymentDialog } from '@/components/projects/Dialog/DeleteDeploymentDialog';
|
||||
import { useToast } from '@/components/shared/Toast';
|
||||
import { Button } from '@/components/ui';
|
||||
import { useGQLClient } from '@/context/GQLClientContext';
|
||||
import { cn } from '@/utils/classnames';
|
||||
import { Deployment, Domain, Environment, Project } from 'gql-client';
|
||||
import {
|
||||
Link,
|
||||
@ -19,18 +20,46 @@ import {
|
||||
Rocket,
|
||||
RotateCw,
|
||||
Trash2,
|
||||
Undo
|
||||
Undo,
|
||||
} from 'lucide-react';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
/**
|
||||
* Props for the DeploymentMenu component.
|
||||
*/
|
||||
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
|
||||
/**
|
||||
* The deployment to display the menu for.
|
||||
*/
|
||||
deployment: Deployment;
|
||||
/**
|
||||
* The current deployment.
|
||||
*/
|
||||
currentDeployment: Deployment;
|
||||
/**
|
||||
* Callback function to update deployments.
|
||||
*/
|
||||
onUpdate: () => Promise<void>;
|
||||
/**
|
||||
* The project the deployment belongs to.
|
||||
*/
|
||||
project: Project;
|
||||
/**
|
||||
* The domains for the production branch of the project.
|
||||
*/
|
||||
prodBranchDomains: Domain[];
|
||||
}
|
||||
|
||||
/**
|
||||
* DeploymentMenu Component
|
||||
*
|
||||
* This component renders a menu for managing deployments, including options to visit the deployment URL,
|
||||
* change the deployment to production, redeploy, rollback, and delete the deployment.
|
||||
* It uses `useGQLClient` to perform actions such as updating, redeploying, rolling back, and deleting deployments.
|
||||
* It also uses `ChangeStateToProductionDialog` and `DeleteDeploymentDialog` for confirmation of actions.
|
||||
*
|
||||
* @param {DeploymentMenuProps} props - The props for the DeploymentMenu component.
|
||||
* @returns {JSX.Element} - The DeploymentMenu component.
|
||||
*/
|
||||
export const DeploymentMenu = ({
|
||||
deployment,
|
||||
currentDeployment,
|
||||
@ -150,9 +179,7 @@ export const DeploymentMenu = ({
|
||||
<Menu placement="bottom-start">
|
||||
<MenuHandler>
|
||||
<Button
|
||||
shape="default"
|
||||
size="xs"
|
||||
variant="unstyled"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'h-8 w-8 rounded-full border border-transparent transition-colors background-transparent',
|
||||
'[&[aria-expanded=true]]:border [&[aria-expanded=true]]:border-border-interactive [&[aria-expanded=true]]:bg-controls-tertiary [&[aria-expanded=true]]:shadow-button',
|
||||
|
@ -1,18 +1,20 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { DatePicker } from 'components/shared/DatePicker';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { Select, SelectOption } from '@/components/shared/Select';
|
||||
import { IconInput } from '@/components/ui';
|
||||
import { Value } from '@/types';
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
Loader2,
|
||||
Search,
|
||||
TrendingUp,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
import { Value } from 'types/vendor';
|
||||
import { DatePicker } from '../../../shared/DatePicker';
|
||||
|
||||
/**
|
||||
* Enum for the status options.
|
||||
*/
|
||||
export enum StatusOptions {
|
||||
ALL_STATUS = 'All status',
|
||||
BUILDING = 'Building',
|
||||
@ -20,19 +22,37 @@ export enum StatusOptions {
|
||||
ERROR = 'Error',
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the filter value.
|
||||
*/
|
||||
export interface FilterValue {
|
||||
searchedBranch: string;
|
||||
status: StatusOptions | string;
|
||||
updateAtRange?: Value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the filter form props.
|
||||
*/
|
||||
interface FilterFormProps {
|
||||
value: FilterValue;
|
||||
onChange: (value: FilterValue) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* FilterForm Component
|
||||
*
|
||||
* This component renders a form for filtering deployments based on branch, status, and date range.
|
||||
* It uses `IconInput` for branch search, `DatePicker` for date range selection, and `Select` for status selection.
|
||||
* The selected filters are applied using the `onChange` prop.
|
||||
*
|
||||
* @param {FilterFormProps} props - The props for the FilterForm component.
|
||||
* @returns {JSX.Element} - The FilterForm component.
|
||||
*/
|
||||
const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
const [searchedBranch, setSearchedBranch] = useState(value.searchedBranch);
|
||||
const [searchedBranch, setSearchedBranch] = useState<string>(
|
||||
value.searchedBranch || '',
|
||||
);
|
||||
const [selectedStatus, setSelectedStatus] = useState(value.status);
|
||||
const [dateRange, setDateRange] = useState<Value>();
|
||||
|
||||
@ -70,24 +90,23 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
leftIcon: getOptionIcon(status),
|
||||
}));
|
||||
|
||||
const handleReset = () => {
|
||||
setSearchedBranch('');
|
||||
};
|
||||
// TODO handle reset with Icon button
|
||||
// const handleReset = () => {
|
||||
// setSearchedBranch('');
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="xl:grid xl:grid-cols-8 flex flex-col xl:gap-3 gap-3">
|
||||
<div className="col-span-4 flex items-center">
|
||||
<Input
|
||||
<div className="xl:grid xl:grid-cols-8 xl:gap-3 flex flex-col gap-3">
|
||||
<div className="flex items-center col-span-4">
|
||||
<IconInput
|
||||
placeholder="Search branches"
|
||||
leftIcon={<Search />}
|
||||
rightIcon={
|
||||
searchedBranch && <X onClick={handleReset} />
|
||||
}
|
||||
// rightIcon={searchedBranch?.length > 0 ? <X onClick={handleReset} /> : undefined}
|
||||
value={searchedBranch}
|
||||
onChange={(e) => setSearchedBranch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center">
|
||||
<div className="flex items-center col-span-2">
|
||||
<DatePicker
|
||||
className="w-full"
|
||||
selectRange
|
||||
@ -96,7 +115,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
onReset={() => setDateRange(undefined)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center">
|
||||
<div className="flex items-center col-span-2">
|
||||
<Select
|
||||
leftIcon={getOptionIcon(selectedStatus as StatusOptions)}
|
||||
options={statusOptions}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { GitCommitWithBranch } from '../../../../../types/types';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import ActivityCard from './ActivityCard';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Heading } from '@/components/shared/Heading';
|
||||
import { Button } from '@/components/ui';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { GitCommitWithBranch } from '../../../../../types/types';
|
||||
import ActivityCard from './ActivityCard';
|
||||
|
||||
export const Activity = ({
|
||||
isLoading,
|
||||
@ -12,16 +12,16 @@ export const Activity = ({
|
||||
activities: GitCommitWithBranch[];
|
||||
}) => {
|
||||
return (
|
||||
<div className="col-span-5 md:col-span-2 mr-1">
|
||||
<div className="md:col-span-2 col-span-5 mr-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Heading className="text-lg leading-6 font-medium">Activity</Heading>
|
||||
<Button variant="tertiary" size="sm">
|
||||
<Heading className="text-lg font-medium leading-6">Activity</Heading>
|
||||
<Button variant="secondary" size="sm">
|
||||
SEE ALL
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
{isLoading ? (
|
||||
<div className="grid place-content-center mt-10">
|
||||
<div className="place-content-center grid mt-10">
|
||||
<Loader className="animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { GitCommitWithBranch } from '../../../../../types/types';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Check, GitBranch } from 'lucide-react';
|
||||
import { Avatar } from '@/components/shared/Avatar';
|
||||
import { IconButton } from '@/components/ui';
|
||||
import { GitCommitWithBranch } from '@/types';
|
||||
import { getInitials } from '@/utils/geInitials';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { Check, GitBranch } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface ActivityCardProps {
|
||||
activity: GitCommitWithBranch;
|
||||
@ -24,7 +24,7 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
|
||||
<Link
|
||||
to={activity.html_url}
|
||||
target="_blank"
|
||||
className="p-3 gap-2 focus-within:ring-2 focus-within:ring-controls-primary/40 focus:outline-none rounded-xl transition-colors hover:bg-base-bg-alternate flex group"
|
||||
className="focus-within:ring-2 focus-within:ring-controls-primary/40 focus:outline-none rounded-xl hover:bg-base-bg-alternate group flex gap-2 p-3 transition-colors"
|
||||
>
|
||||
<div>
|
||||
<Avatar
|
||||
@ -38,7 +38,7 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
|
||||
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
||||
{activity.commit.author?.name}
|
||||
</span>
|
||||
<span className="text-elements-low-em text-xs flex items-center gap-2">
|
||||
<span className="text-elements-low-em flex items-center gap-2 text-xs">
|
||||
<span title={formattedDate} className="whitespace-nowrap">
|
||||
{formattedDate}
|
||||
</span>
|
||||
@ -51,7 +51,7 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
|
||||
<span
|
||||
title={activity.branch.name}
|
||||
// pseudo to increase hover area
|
||||
className="before:absolute relative before:h-5 before:-top-4 before:inset-x-0"
|
||||
className="before:absolute before:h-5 before:-top-4 before:inset-x-0 relative"
|
||||
>
|
||||
<span className="line-clamp-1">{activity.branch.name}</span>
|
||||
</span>
|
||||
@ -59,17 +59,16 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
|
||||
</span>
|
||||
<p
|
||||
title={activity.commit.message}
|
||||
className="text-sm line-clamp-4 tracking-tight text-elements-mid-em mt-2"
|
||||
className="line-clamp-4 text-elements-mid-em mt-2 text-sm tracking-tight"
|
||||
>
|
||||
{activity.commit.message}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
<IconButton
|
||||
aria-label="Go to commit"
|
||||
variant="unstyled"
|
||||
tabIndex={-1}
|
||||
className="p-0 text-elements-low-em group-focus-within:opacity-100 group-hover:opacity-100 opacity-0 transition-all"
|
||||
leftIcon={<Check className="w-5 h-5" />}
|
||||
className="text-elements-low-em group-focus-within:opacity-100 group-hover:opacity-100 p-0 transition-all opacity-0"
|
||||
leftIcon={<Check />}
|
||||
/>
|
||||
</Link>
|
||||
{/* Separator calc => 100% - 36px (avatar) - 12px (padding-left) - 8px (gap)
|
||||
|
@ -8,9 +8,11 @@ import {
|
||||
DialogTitle,
|
||||
} from '@mui/material';
|
||||
|
||||
import { Button, Heading, Tag } from 'components/shared';
|
||||
import { Heading } from '@/components/shared/Heading';
|
||||
import { Tag } from '@/components/shared/Tag';
|
||||
import { Button } from '@/components/ui';
|
||||
import { useGQLClient } from '@/context/GQLClientContext';
|
||||
import { CheckCircle, Loader2 } from 'lucide-react';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
|
||||
const WAIT_DURATION = 5000;
|
||||
|
||||
@ -82,9 +84,9 @@ export const AuctionCard = ({ project }: { project: Project }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-3 gap-2 rounded-xl border dark:border-overlay3 border-gray-200 transition-colors hover:bg-base-bg-alternate dark:hover:bg-overlay3 flex flex-col mt-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<Heading className="text-lg leading-6 font-medium">
|
||||
<div className="rounded-xl dark:border-overlay3 hover:bg-base-bg-alternate dark:hover:bg-overlay3 flex flex-col gap-2 p-3 mt-8 transition-colors border border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<Heading className="text-lg font-medium leading-6">
|
||||
Auction details
|
||||
</Heading>
|
||||
<Button onClick={handleOpenDialog} variant="secondary" size="sm">
|
||||
@ -92,7 +94,7 @@ export const AuctionCard = ({ project }: { project: Project }) => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
|
||||
Auction Id
|
||||
</span>
|
||||
@ -101,7 +103,7 @@ export const AuctionCard = ({ project }: { project: Project }) => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mt-1">
|
||||
<div className="flex items-center justify-between mt-1">
|
||||
<span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
|
||||
Auction Status
|
||||
</span>
|
||||
@ -124,7 +126,7 @@ export const AuctionCard = ({ project }: { project: Project }) => {
|
||||
</p>
|
||||
))}
|
||||
|
||||
<div className="flex justify-between items-center mt-1">
|
||||
<div className="flex items-center justify-between mt-1">
|
||||
<span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
|
||||
Deployer Funds Status
|
||||
</span>
|
||||
@ -169,9 +171,7 @@ export const AuctionCard = ({ project }: { project: Project }) => {
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={handleCloseDialog}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { cloneElement } from '@/utils/cloneElement';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { cloneElement } from 'utils/cloneElement';
|
||||
|
||||
interface OverviewInfoProps {
|
||||
label: string;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { UseFormRegister } from 'react-hook-form';
|
||||
|
||||
import { EnvironmentVariablesFormValues } from '../../../../types';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Trash } from 'lucide-react';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { EnvironmentVariablesFormValues } from '../../../../types';
|
||||
|
||||
interface AddEnvironmentVariableRowProps {
|
||||
onDelete: () => void;
|
||||
@ -19,22 +19,20 @@ const AddEnvironmentVariableRow = ({
|
||||
isDeleteDisabled,
|
||||
}: AddEnvironmentVariableRowProps) => {
|
||||
return (
|
||||
<div className="flex gap-2 py-1 self-stretch items-end">
|
||||
<div className="flex items-end self-stretch gap-2 py-1">
|
||||
<Input
|
||||
size="md"
|
||||
{...register(`variables.${index}.key`, {
|
||||
required: 'Key field cannot be empty',
|
||||
})}
|
||||
label={index === 0 ? 'Key' : undefined}
|
||||
placeholder={index === 0 ? 'Key' : undefined}
|
||||
/>
|
||||
<Input
|
||||
size="md"
|
||||
label={index === 0 ? 'Value' : undefined}
|
||||
placeholder={index === 0 ? 'Value' : undefined}
|
||||
{...register(`variables.${index}.value`, {
|
||||
required: 'Value field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
<Button size="md" iconOnly onClick={onDelete} disabled={isDeleteDisabled}>
|
||||
<Button size="icon" onClick={onDelete} disabled={isDeleteDisabled}>
|
||||
<Trash />
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -3,10 +3,10 @@ import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Typography } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Modal } from 'components/shared/Modal';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { Modal } from '@/components/shared/Modal';
|
||||
import { Select, SelectOption } from '@/components/shared/Select';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { AddProjectMemberInput, Permission } from 'gql-client';
|
||||
|
||||
interface AddMemberDialogProp {
|
||||
@ -93,10 +93,10 @@ const AddMemberDialog = ({
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={handleOpen} variant="danger" shape="default">
|
||||
<Button onClick={handleOpen} variant="destructive">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!isValid} shape="default">
|
||||
<Button type="submit" disabled={!isValid}>
|
||||
SEND INVITE
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { Modal } from 'components/shared/Modal';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Modal } from '@/components/shared/Modal';
|
||||
import { useToast } from '@/components/shared/Toast';
|
||||
import { Button, Input } from '@/components/ui';
|
||||
import { useGQLClient } from '@/context';
|
||||
import { FormHelperText } from '@mui/material';
|
||||
import { Project } from 'gql-client';
|
||||
|
||||
interface DeleteProjectDialogProp {
|
||||
@ -59,7 +59,7 @@ const DeleteProjectDialog = ({
|
||||
<form onSubmit={handleSubmit(deleteProjectHandler)}>
|
||||
<Modal.Body>
|
||||
<Input
|
||||
label={
|
||||
title={
|
||||
"Deleting your project is irreversible. Enter your project's name " +
|
||||
'"' +
|
||||
project.name +
|
||||
@ -71,14 +71,17 @@ const DeleteProjectDialog = ({
|
||||
required: 'Project name is required',
|
||||
validate: (value) => value === project.name,
|
||||
})}
|
||||
helperText="Deleting your project is irreversible."
|
||||
// helperText="Deleting your project is irreversible."
|
||||
/>
|
||||
<FormHelperText>
|
||||
Deleting your project is irreversible.
|
||||
</FormHelperText>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="flex justify-start">
|
||||
<Button onClick={handleOpen} variant="tertiary">
|
||||
<Button onClick={handleOpen} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" type="submit" disabled={!isValid}>
|
||||
<Button variant="destructive" type="submit" disabled={!isValid}>
|
||||
Yes, delete project
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user