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:
Ian Cameron Lyles 2025-02-24 20:20:23 -08:00 committed by GitHub
parent a1be980976
commit 52512beaa2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
249 changed files with 11663 additions and 12479 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ packages/frontend/dist/
# ignore all .DS_Store files # ignore all .DS_Store files
**/.DS_Store **/.DS_Store
.vscode

View File

@ -1,9 +0,0 @@
{
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
"tv\\('([^)]*)\\')",
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
],
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}

View File

@ -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 { import {
ApolloServerPluginDrainHttpServer, ApolloServerPluginDrainHttpServer,
ApolloServerPluginLandingPageLocalDefault, ApolloServerPluginLandingPageLocalDefault,
AuthenticationError, AuthenticationError,
} from 'apollo-server-core'; } 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 session from 'express-session';
import { createServer } from 'http';
import { TypeSource } from '@graphql-tools/utils';
import { makeExecutableSchema } from '@graphql-tools/schema'; import { makeExecutableSchema } from '@graphql-tools/schema';
import { TypeSource } from '@graphql-tools/utils';
import { ServerConfig } from './config'; import { ServerConfig } from './config';
import { DEFAULT_GQL_PATH } from './constants'; import { DEFAULT_GQL_PATH } from './constants';
import githubRouter from './routes/github';
import authRouter from './routes/auth'; import authRouter from './routes/auth';
import githubRouter from './routes/github';
import stagingRouter from './routes/staging'; import stagingRouter from './routes/staging';
import { Service } from './service'; import { Service } from './service';
@ -101,7 +101,7 @@ export const createAndStartServer = async (
} }
app.use( app.use(
session(sessionOptions) session(sessionOptions) as unknown as express.RequestHandler
); );
server.applyMiddleware({ server.applyMiddleware({
@ -116,9 +116,9 @@ export const createAndStartServer = async (
app.use(express.json()); app.use(express.json());
app.set('service', service); app.set('service', service);
app.use('/auth', authRouter); app.use('/auth', authRouter as express.RequestHandler);
app.use('/api/github', githubRouter); app.use('/api/github', githubRouter as express.RequestHandler);
app.use('/staging', stagingRouter); app.use('/staging', stagingRouter as express.RequestHandler);
app.use((err: any, req: any, res: any, next: any) => { app.use((err: any, req: any, res: any, next: any) => {
console.error(err); console.error(err);

View File

@ -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>,
},
],
};

View File

@ -115,7 +115,7 @@
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/lodash": "^4.17.0", "@types/lodash": "^4.17.0",
"@types/luxon": "^3.3.7", "@types/luxon": "^3.3.7",
"@types/node": "^16.18.68", "@types/node": "^22.13.5",
"@types/react": "^18.2.66", "@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",

View File

@ -1,37 +1,32 @@
import { ThemeProvider } from 'next-themes';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { DashboardLayout } from './layouts/DashboardLayout';
import { BASE_URL } from 'utils/constants';
import ProjectSearchLayout from './layouts/ProjectSearch';
import Index from './pages'; import Index from './pages';
import AuthPage from './pages/AuthPage'; import AuthPage from './pages/AuthPage';
import BuyPrepaidService from './pages/BuyPrepaidService'; 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 Settings from './pages/org-slug/Settings';
import { DashboardLayout } from './pages/org-slug/layout'; import { BASE_URL } from './utils/constants';
import {
projectsRoutesWithoutSearch,
projectsRoutesWithSearch,
} from './pages/org-slug/projects/routes';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
path: ':orgSlug', path: ':orgSlug',
element: <DashboardLayout />, element: <DashboardLayout />,
children: [ children: [
{ // {
element: <ProjectSearchLayout />, // element: <ProjectSearchLayout />,
children: [ // children: [
{ // {
path: '', // path: '',
element: <Projects />, // element: <Projects />,
}, // },
{ // {
path: 'projects', // path: 'projects',
children: projectsRoutesWithSearch, // children: projectsRoutesWithSearch,
}, // },
], // ],
}, // },
{ {
path: 'settings', path: 'settings',
element: <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() { function App() {
// Hacky way of checking session // Hacky way of checking session
// TODO: Handle redirect backs // TODO: Handle redirect backs
@ -66,7 +66,6 @@ function App() {
const path = window.location.pathname; const path = window.location.pathname;
if (res.status !== 200) { if (res.status !== 200) {
localStorage.clear(); localStorage.clear();
if (path !== '/login') { if (path !== '/login') {
window.location.pathname = '/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; export default App;

View File

@ -1,8 +1,8 @@
import { import {
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO, VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO,
VITE_GITHUB_PWA_TEMPLATE_REPO,
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO, VITE_GITHUB_NEXT_APP_TEMPLATE_REPO,
} from 'utils/constants'; VITE_GITHUB_PWA_TEMPLATE_REPO,
} from '@/utils/constants';
export default [ export default [
{ {

View File

@ -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>
);
};

View File

@ -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;

View File

@ -1,6 +1,6 @@
import { cn } from '@/utils/classnames';
import { Duration } from 'luxon'; import { Duration } from 'luxon';
import { ComponentPropsWithoutRef } from 'react'; import { ComponentPropsWithoutRef } from 'react';
import { cn } from 'utils/classnames';
export interface FormatMilliSecondProps export interface FormatMilliSecondProps
extends ComponentPropsWithoutRef<'div'> { extends ComponentPropsWithoutRef<'div'> {

View File

@ -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>
)
}

View File

@ -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>
);
}

View File

@ -1,7 +1,7 @@
import React, { forwardRef, RefAttributes } from 'react'; import React, { forwardRef, RefAttributes } from 'react';
import { IconInput, InputProps } from '@/components/ui';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import { Input, InputProps } from './shared/Input';
const SearchBar: React.ForwardRefRenderFunction< const SearchBar: React.ForwardRefRenderFunction<
HTMLInputElement, HTMLInputElement,
@ -9,13 +9,13 @@ const SearchBar: React.ForwardRefRenderFunction<
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => { > = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
return ( return (
<div className="relative flex w-full"> <div className="relative flex w-full">
<Input <IconInput
leftIcon={<Search className="text-foreground-secondary" />} leftIcon={<Search className="text-foreground-secondary" />}
onChange={onChange} onChange={onChange}
value={value} value={value}
type="search" type="search"
placeholder={placeholder} placeholder={placeholder}
appearance="borderless" // appearance="borderless"
className="w-full lg:w-[459px]" className="w-full lg:w-[459px]"
{...props} {...props}
ref={ref} ref={ref}

View File

@ -1,11 +0,0 @@
import {
AlertDescription,
AlertTitle,
Alert as UIAlert,
} from '@/components/ui/alert';
export const Alert = UIAlert;
export {
AlertDescription,
AlertTitle
};

View File

@ -1,10 +0,0 @@
import {
AvatarFallback,
AvatarImage,
Avatar as UIAvatar,
} from '@/components/ui/avatar';
export const Avatar = UIAvatar;
export {
AvatarFallback, AvatarImage
};

View File

@ -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 };

View File

@ -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} />;
}

View File

@ -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 };

View File

@ -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 };

View File

@ -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>;

View File

@ -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>
);
}

View File

@ -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}
/>
);
}

View File

@ -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>
);
}

View File

@ -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'>;

View File

@ -1,11 +0,0 @@
import {
InputOTPGroup,
InputOTPSlot,
InputOTP as UIInputOTP,
} from '@/components/ui/input-otp';
export const InputOTP = UIInputOTP;
export {
InputOTPGroup,
InputOTPSlot
};

View File

@ -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

View File

@ -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>
);
}

View File

@ -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>;

View File

@ -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
};

View File

@ -1,3 +0,0 @@
import { Separator as UISeparator } from '@/components/ui/separator';
export const Separator = UISeparator;

View File

@ -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
};

View File

@ -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>;

View File

@ -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
};

View File

@ -1,12 +0,0 @@
import {
TabsContent,
TabsList,
TabsTrigger,
Tabs as UITabs,
} from '@/components/ui/tabs';
export const Tabs = UITabs;
export {
TabsContent, TabsList,
TabsTrigger
};

View File

@ -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>
);
}

View File

@ -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 };

View File

@ -1,7 +0,0 @@
import {
ToggleGroupItem,
ToggleGroup as UIToggleGroup,
} from '@/components/ui/toggle-group';
export const ToggleGroup = UIToggleGroup;
export { ToggleGroupItem };

View File

@ -1,13 +0,0 @@
import {
TooltipContent,
TooltipProvider,
TooltipTrigger,
Tooltip as UITooltip,
} from '@/components/ui/tooltip';
export const Tooltip = UITooltip;
export {
TooltipContent,
TooltipProvider,
TooltipTrigger
};

View File

@ -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>
);
}

View 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>
);
}

View File

@ -0,0 +1 @@
export * from './ui';

View 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';

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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 { interface LaconicIconProps {
className?: string /**
width?: number * Optional CSS class names to apply to the component.
height?: number */
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 ( return (
<svg <svg
width={width} width={width}
@ -23,6 +47,5 @@ export const LaconicIcon: React.FC<LaconicIconProps> = ({ className = "", width
className="fill-current" className="fill-current"
/> />
</svg> </svg>
) );
} };

View File

@ -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>
);
}

View File

@ -1,16 +1,31 @@
import type React from 'react'; import type React from 'react';
/**
* WalletSessionIdProps interface defines the props for the WalletSessionId component.
*/
interface WalletSessionIdProps { interface WalletSessionIdProps {
/**
* The wallet ID to display.
*/
walletId?: string; walletId?: string;
/**
* Optional CSS class names to apply to the component.
*/
className?: string; 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> = ({ export const WalletSessionId: React.FC<WalletSessionIdProps> = ({
walletId, walletId,
className = '', className = '',
}) => { }) => {
// const { wallet } = useWallet(); // const { wallet } = useWallet();
const wallet = {id: 'x123xxx'} const wallet = { id: 'x123xxx' };
const displayId = walletId || wallet?.id || 'Not Connected'; const displayId = walletId || wallet?.id || 'Not Connected';
return ( return (

View File

@ -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';

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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 };

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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 };

View File

@ -0,0 +1,2 @@
export * from './ProjectSearchBar';
export * from './ProjectSearchBarDialog';

View 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>
);
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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> */}
</>
);
}

View File

@ -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>
)
}

View File

@ -1,6 +1,6 @@
import ConfirmDialog, { import ConfirmDialog, {
ConfirmDialogProps, ConfirmDialogProps,
} from 'components/shared/ConfirmDialog'; } from '@/components/shared/ConfirmDialog';
interface CancelDeploymentDialogProps extends ConfirmDialogProps {} interface CancelDeploymentDialogProps extends ConfirmDialogProps {}
@ -18,7 +18,7 @@ export const CancelDeploymentDialog = ({
open={open} open={open}
confirmButtonTitle="Yes, cancel deployment" confirmButtonTitle="Yes, cancel deployment"
handleConfirm={handleConfirm} handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }} confirmButtonProps={{ variant: 'destructive' }}
> >
<p className="text-sm text-elements-high-em tracking-[-0.006em]"> <p className="text-sm text-elements-high-em tracking-[-0.006em]">
This will halt the deployment and you&apos;ll have to start the process This will halt the deployment and you&apos;ll have to start the process

View File

@ -1,17 +1,18 @@
import ConfirmDialog, { import ConfirmDialog, {
ConfirmDialogProps, ConfirmDialogProps,
} from 'components/shared/ConfirmDialog'; } from '@/components/shared/ConfirmDialog';
import { Deployment, Domain } from 'gql-client'; import { Deployment, Domain } from 'gql-client';
import { Link } from 'react-router-dom';
import DeploymentDialogBodyCard from 'components/projects/project/deployments/DeploymentDialogBodyCard'; import DeploymentDialogBodyCard from '@/components/projects/project/deployments/DeploymentDialogBodyCard';
import { Button } from 'components/shared/Button'; import { TagProps } from '@/components/shared/Tag';
import { Button } from '@/components/ui';
import { import {
ChevronDown,
Link,
ArrowRightCircle, ArrowRightCircle,
ChevronDown,
Link as LinkIcon,
Loader2, Loader2,
} from 'lucide-react'; } from 'lucide-react';
import { TagProps } from 'components/shared/Tag';
interface ChangeStateToProductionDialogProps extends ConfirmDialogProps { interface ChangeStateToProductionDialogProps extends ConfirmDialogProps {
deployment: Deployment; 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"> <div className="flex flex-col gap-3">
<p className="text-sm text-elements-high-em tracking-[-0.006em]"> <p className="text-sm text-elements-high-em tracking-[-0.006em]">
Upon confirmation, this deployment will be changed to production. Upon confirmation, this deployment will be changed to production.
@ -66,7 +67,7 @@ export const ChangeStateToProductionDialog = ({
/> />
{newDeployment && ( {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) => ( {Array.from({ length: 7 }).map((_, index) => (
<ChevronDown key={index} /> <ChevronDown key={index} />
))} ))}
@ -85,15 +86,16 @@ export const ChangeStateToProductionDialog = ({
{domains.length > 0 && {domains.length > 0 &&
domains.map((value) => { domains.map((value) => {
return ( return (
<Button <Link to={value.name}>
as="a" <Button
href={value.name} leftIcon={<LinkIcon size={18} />}
leftIcon={<Link size={18} />} // variant="link"
variant="link" key={value.id}
key={value.id} asChild
> >
{value.name} {value.name}
</Button> </Button>
</Link>
); );
})} })}
</div> </div>

View File

@ -1,10 +1,7 @@
import ConfirmDialog, { import ConfirmDialog, {
ConfirmDialogProps, ConfirmDialogProps,
} from 'components/shared/ConfirmDialog'; } from '@/components/shared/ConfirmDialog';
import { import { AlertTriangle } from 'lucide-react';
AlertTriangle,
} from 'lucide-react';
interface DeleteDeploymentDialogProps extends ConfirmDialogProps { interface DeleteDeploymentDialogProps extends ConfirmDialogProps {
isConfirmButtonLoading?: boolean; isConfirmButtonLoading?: boolean;
@ -30,7 +27,7 @@ export const DeleteDeploymentDialog = ({
} }
handleConfirm={handleConfirm} handleConfirm={handleConfirm}
confirmButtonProps={{ confirmButtonProps={{
variant: 'danger', variant: 'destructive',
disabled: isConfirmButtonLoading, disabled: isConfirmButtonLoading,
rightIcon: isConfirmButtonLoading ? ( rightIcon: isConfirmButtonLoading ? (
<AlertTriangle className="animate-spin" /> <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. Once deleted, the deployment will not be accessible.
</p> </p>
</ConfirmDialog> </ConfirmDialog>

View File

@ -1,6 +1,6 @@
import ConfirmDialog, { import ConfirmDialog, {
ConfirmDialogProps, ConfirmDialogProps,
} from 'components/shared/ConfirmDialog'; } from '@/components/shared/ConfirmDialog';
interface DeleteDomainDialogProps extends ConfirmDialogProps { interface DeleteDomainDialogProps extends ConfirmDialogProps {
projectName: string; projectName: string;
@ -23,9 +23,9 @@ export const DeleteDomainDialog = ({
open={open} open={open}
confirmButtonTitle="Yes, delete domain" confirmButtonTitle="Yes, delete domain"
handleConfirm={handleConfirm} 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{' '} Once deleted, the project{' '}
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5"> <span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
{projectName} {projectName}

View File

@ -1,6 +1,6 @@
import ConfirmDialog, { import ConfirmDialog, {
ConfirmDialogProps, ConfirmDialogProps,
} from 'components/shared/ConfirmDialog'; } from '@/components/shared/ConfirmDialog';
interface DeleteVariableDialogProps extends ConfirmDialogProps { interface DeleteVariableDialogProps extends ConfirmDialogProps {
variableKey: string; variableKey: string;
@ -21,9 +21,9 @@ export const DeleteVariableDialog = ({
open={open} open={open}
confirmButtonTitle="Yes, confirm delete" confirmButtonTitle="Yes, confirm delete"
handleConfirm={handleConfirm} 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{' '} 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"> <span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
{variableKey} {variableKey}

View File

@ -1,6 +1,6 @@
import ConfirmDialog, { import ConfirmDialog, {
ConfirmDialogProps, ConfirmDialogProps,
} from 'components/shared/ConfirmDialog'; } from '@/components/shared/ConfirmDialog';
interface DeleteWebhookDialogProps extends ConfirmDialogProps { interface DeleteWebhookDialogProps extends ConfirmDialogProps {
webhookUrl: string; webhookUrl: string;
@ -21,9 +21,9 @@ export const DeleteWebhookDialog = ({
open={open} open={open}
confirmButtonTitle="Yes, confirm delete" confirmButtonTitle="Yes, confirm delete"
handleConfirm={handleConfirm} 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{' '} Are you sure you want to delete{' '}
<span className="text-sm font-mono text-elements-high-em px-0.5"> <span className="text-sm font-mono text-elements-high-em px-0.5">
{webhookUrl} {webhookUrl}

View File

@ -1,6 +1,6 @@
import ConfirmDialog, { import ConfirmDialog, {
ConfirmDialogProps, ConfirmDialogProps,
} from 'components/shared/ConfirmDialog'; } from '@/components/shared/ConfirmDialog';
interface DisconnectRepositoryDialogProps extends ConfirmDialogProps {} interface DisconnectRepositoryDialogProps extends ConfirmDialogProps {}
@ -18,9 +18,9 @@ export const DisconnectRepositoryDialog = ({
open={open} open={open}
confirmButtonTitle="Yes, confirm disconnect" confirmButtonTitle="Yes, confirm disconnect"
handleConfirm={handleConfirm} 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 Any data tied to your Git project may become misconfigured. Are you sure
you want to continue? you want to continue?
</p> </p>

View File

@ -1,8 +1,8 @@
import ConfirmDialog, { import ConfirmDialog, {
ConfirmDialogProps, ConfirmDialogProps,
} from 'components/shared/ConfirmDialog'; } from '@/components/shared/ConfirmDialog';
import { formatAddress } from 'utils/format'; import { formatAddress } from '@/utils/format';
interface RemoveMemberDialogProps extends ConfirmDialogProps { interface RemoveMemberDialogProps extends ConfirmDialogProps {
memberName: string; memberName: string;
@ -27,9 +27,9 @@ export const RemoveMemberDialog = ({
open={open} open={open}
confirmButtonTitle="Yes, remove member" confirmButtonTitle="Yes, remove member"
handleConfirm={handleConfirm} 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)}@ Once removed, {formatAddress(memberName)} ({formatAddress(ethAddress)}@
{emailDomain}) will not be able to access this project. {emailDomain}) will not be able to access this project.
</p> </p>

View File

@ -1,6 +1,6 @@
import ConfirmDialog, { import ConfirmDialog, {
ConfirmDialogProps, ConfirmDialogProps,
} from 'components/shared/ConfirmDialog'; } from '@/components/shared/ConfirmDialog';
interface TransferProjectDialogProps extends ConfirmDialogProps { interface TransferProjectDialogProps extends ConfirmDialogProps {
projectName: string; projectName: string;

View File

@ -1,28 +1,24 @@
import { WavyBorder } from '@/components/shared/WavyBorder';
import { Button } from '@/components/ui';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { import {
Menu, DropdownMenu,
MenuHandler, DropdownMenuContent,
MenuItem, DropdownMenuItem,
MenuList, DropdownMenuTrigger,
} from '@snowballtools/material-tailwind-react-fork'; } from '@/components/ui/dropdown-menu';
import { import {
ComponentPropsWithoutRef, Tooltip,
MouseEvent, TooltipContent,
useCallback, TooltipProvider,
} from 'react'; TooltipTrigger,
import { useNavigate } from 'react-router-dom'; } from '@/components/ui/tooltip';
import { getInitials } from '@/utils/geInitials';
import { relativeTimeMs } from '@/utils/time';
import { Project } from 'gql-client'; import { Project } from 'gql-client';
import { Avatar } from 'components/shared/Avatar'; import { AlertTriangle, Clock, GitBranch, MoreHorizontal } from 'lucide-react';
import { Button } from 'components/shared/Button'; import { ComponentPropsWithoutRef, MouseEvent, useCallback } from 'react';
import { import { useNavigate } from 'react-router-dom';
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 { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme'; import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
export interface ProjectCardProps export interface ProjectCardProps
@ -66,6 +62,25 @@ export const ProjectCard = ({
[project.id, navigate], [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 ( return (
<div <div
{...props} {...props}
@ -75,50 +90,51 @@ export const ProjectCard = ({
{/* Upper content */} {/* Upper content */}
<div className={theme.upperContent()}> <div className={theme.upperContent()}>
{/* Icon container */} {/* Icon container */}
<Avatar <Avatar className="w-12 h-12">
size={48} <AvatarImage src={project.icon} alt={project.name} />
imageSrc={project.icon} <AvatarFallback>{getInitials(project.name)}</AvatarFallback>
initials={getInitials(project.name)} </Avatar>
/>
{/* Title and website */} {/* Title and website */}
<div className={theme.content()}> <div className={theme.content()}>
<Tooltip content={project.name}> <TooltipProvider>
<p className={theme.title()}>{project.name}</p> <Tooltip>
</Tooltip> <TooltipTrigger asChild>
<p className={theme.title()}>{project.name}</p>
</TooltipTrigger>
<TooltipContent>
<p>{project.name}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<p className={theme.description()}> <p className={theme.description()}>
{project.deployments[0]?.applicationDeploymentRecordData?.url ?? 'No domain'} {project.deployments[0]?.applicationDeploymentRecordData?.url ??
'No domain'}
</p> </p>
</div> </div>
{/* Icons */} {/* Icons */}
<div className={theme.icons()}> <div className={theme.icons()}>
{hasError && <AlertTriangle className="text-error" />} {hasError && <AlertTriangle className="text-error" />}
<Menu placement="bottom-end"> <DropdownMenu>
<MenuHandler> <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<Button <Button variant="ghost">
shape="default" <MoreHorizontal className="w-4 h-4" />
size="xs"
variant="ghost"
iconOnly
onClick={handleOptionsClick}
>
<MoreHorizontal />
</Button> </Button>
</MenuHandler> </DropdownMenuTrigger>
<MenuList className="dark:bg-overlay3 dark:shadow-background dark:border-none"> <DropdownMenuContent
<MenuItem align="end"
onClick={navigateToSettingsOnClick} onClick={(e) => e.stopPropagation()}
className="text-foreground" >
> <DropdownMenuItem onClick={handleSettingsClick}>
Project settings Project settings
</MenuItem> </DropdownMenuItem>
<MenuItem <DropdownMenuItem
onClick={handleDeleteClick}
className="text-error" className="text-error"
onClick={navigateToSettingsOnClick}
> >
Delete project Delete project
</MenuItem> </DropdownMenuItem>
</MenuList> </DropdownMenuContent>
</Menu> </DropdownMenu>
</div> </div>
</div> </div>
{/* Wave */} {/* Wave */}

View File

@ -1,13 +1,13 @@
import { useCallback, useEffect, useState } from 'react';
import { useCombobox } from 'downshift'; import { useCombobox } from 'downshift';
import { Project } from 'gql-client'; import { Project } from 'gql-client';
import { useCallback, useEffect, useState } from 'react';
import { useDebounceValue } from 'usehooks-ts'; import { useDebounceValue } from 'usehooks-ts';
import SearchBar from 'components/SearchBar'; import SearchBar from '@/components/SearchBar';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from '@/context/GQLClientContext';
import { cn } from 'utils/classnames'; import { cn } from '@/utils/classnames';
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty'; import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
interface ProjectSearchBarProps { interface ProjectSearchBarProps {
onChange?: (data: Project) => void; onChange?: (data: Project) => void;

View File

@ -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 * 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 { 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 { interface ProjectSearchBarDialogProps extends Dialog.DialogProps {
open?: boolean; open?: boolean;
@ -26,6 +26,7 @@ export const ProjectSearchBarDialog = ({
const [selectedItem, setSelectedItem] = useState<Project | null>(null); const [selectedItem, setSelectedItem] = useState<Project | null>(null);
const client = useGQLClient(); const client = useGQLClient();
const navigate = useNavigate(); const navigate = useNavigate();
const inputRef = useRef<HTMLInputElement>(null);
const { const {
getInputProps, getInputProps,
@ -75,22 +76,32 @@ export const ProjectSearchBarDialog = ({
return ( return (
<Dialog.Root {...props}> <Dialog.Root {...props}>
<Dialog.Portal> <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> <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="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
<Input <div className="relative flex-1">
{...getInputProps({}, { suppressRefError: true })} <Search className="left-2 top-1/2 text-muted-foreground absolute w-4 h-4 -translate-y-1/2" />
leftIcon={<Search />} <Input
placeholder="Search" {...getInputProps(
appearance="borderless" { ref: inputRef },
autoFocus { suppressRefError: true },
/> )}
<Button iconOnly variant="ghost" onClick={handleClose}> className="pl-8"
<X size={16} /> placeholder="Search"
autoFocus
type="text"
/>
</div>
<Button
variant="ghost"
size="icon"
onClick={handleClose}
type="button"
>
<X className="w-4 h-4" />
</Button> </Button>
</div> </div>
{/* Content */}
<div <div
className="flex flex-col gap-1 px-2 py-2" className="flex flex-col gap-1 px-2 py-2"
{...getMenuProps( {...getMenuProps(

View File

@ -1,6 +1,6 @@
import { cn } from '@/utils/classnames';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import { ComponentPropsWithoutRef } from 'react'; import { ComponentPropsWithoutRef } from 'react';
import { cn } from 'utils/classnames';
interface ProjectSearchBarEmptyProps extends ComponentPropsWithoutRef<'div'> {} interface ProjectSearchBarEmptyProps extends ComponentPropsWithoutRef<'div'> {}

View File

@ -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 { Overwrite, UseComboboxGetItemPropsReturnValue } from 'downshift';
import { Project } from 'gql-client'; import { Project } from 'gql-client';
import { ComponentPropsWithoutRef, forwardRef } from 'react'; 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. * Represents a type that merges ComponentPropsWithoutRef<'li'> with certain exclusions.

View File

@ -1,10 +1,20 @@
import { Modal, ModalContent } from 'components/shared'; import { Modal } from '@/components/shared/Modal';
import { useCallback, useEffect } from 'react'; import { ModalContent } from '@/components/shared/Modal/ModalContent';
// import { Modal } from '@mui/material';
import { import {
VITE_LACONICD_CHAIN_ID, VITE_LACONICD_CHAIN_ID,
VITE_WALLET_IFRAME_URL, 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 = ({ const ApproveTransactionModal = ({
setAccount, setAccount,
setIsDataReceived, setIsDataReceived,
@ -22,7 +32,9 @@ const ApproveTransactionModal = ({
setIsDataReceived(true); setIsDataReceived(true);
if (event.data.data.length === 0) { 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; return;
} }
@ -41,6 +53,11 @@ const ApproveTransactionModal = ({
}; };
}, []); }, []);
/**
* Gets the data from the wallet.
* @function getDataFromWallet
* @returns {void}
*/
const getDataFromWallet = useCallback(() => { const getDataFromWallet = useCallback(() => {
const iframe = document.getElementById('walletIframe') as HTMLIFrameElement; const iframe = document.getElementById('walletIframe') as HTMLIFrameElement;
@ -59,7 +76,7 @@ const ApproveTransactionModal = ({
}, []); }, []);
return ( 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"> <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 <iframe
onLoad={getDataFromWallet} onLoad={getDataFromWallet}

View File

@ -2,12 +2,20 @@ import { useEffect, useState } from 'react';
import { Modal } from '@mui/material'; import { Modal } from '@mui/material';
import { VITE_WALLET_IFRAME_URL } from 'utils/constants'; import { VITE_WALLET_IFRAME_URL } from '@/utils/constants';
import useCheckBalance from '../../../hooks/useCheckBalance'; import useCheckBalance from '@/hooks/useCheckBalance';
const CHECK_BALANCE_INTERVAL = 5000; const CHECK_BALANCE_INTERVAL = 5000;
const IFRAME_ID = 'checkBalanceIframe'; 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 = ({ const CheckBalanceIframe = ({
onBalanceChange, onBalanceChange,
isPollingEnabled, isPollingEnabled,
@ -24,6 +32,9 @@ const CheckBalanceIframe = ({
const [isLoaded, setIsLoaded] = useState(false); const [isLoaded, setIsLoaded] = useState(false);
/**
* useEffect hook that calls checkBalance when the component is loaded or the amount changes.
*/
useEffect(() => { useEffect(() => {
if (!isLoaded) { if (!isLoaded) {
return; return;
@ -31,6 +42,10 @@ const CheckBalanceIframe = ({
checkBalance(); checkBalance();
}, [amount, checkBalance, isLoaded]); }, [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(() => { useEffect(() => {
if (!isPollingEnabled || !isLoaded || isBalanceSufficient) { if (!isPollingEnabled || !isLoaded || isBalanceSufficient) {
return; return;
@ -45,9 +60,12 @@ const CheckBalanceIframe = ({
}; };
}, [isBalanceSufficient, isPollingEnabled, checkBalance, isLoaded]); }, [isBalanceSufficient, isPollingEnabled, checkBalance, isLoaded]);
/**
* useEffect hook that calls the onBalanceChange callback when the isBalanceSufficient state changes.
*/
useEffect(() => { useEffect(() => {
onBalanceChange(isBalanceSufficient); onBalanceChange(isBalanceSufficient);
}, [isBalanceSufficient]); }, [isBalanceSufficient, onBalanceChange]);
return ( return (
<Modal open={false} disableEscapeKeyDown keepMounted> <Modal open={false} disableEscapeKeyDown keepMounted>

View File

@ -5,23 +5,29 @@ import {
Deployer, Deployer,
} from 'gql-client'; } from 'gql-client';
import { useCallback, useEffect, useMemo, useState } from 'react'; 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 { useNavigate, useSearchParams } from 'react-router-dom';
import { useMediaQuery } from 'usehooks-ts'; 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 { import {
VITE_LACONICD_CHAIN_ID, VITE_LACONICD_CHAIN_ID,
VITE_WALLET_IFRAME_URL, 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 { useGQLClient } from '../../../context/GQLClientContext';
import { Button } from '../../shared/Button';
import { Heading } from '../../shared/Heading'; import { Heading } from '../../shared/Heading';
import ApproveTransactionModal from './ApproveTransactionModal'; import ApproveTransactionModal from './ApproveTransactionModal';
import CheckBalanceIframe from './CheckBalanceIframe'; import CheckBalanceIframe from './CheckBalanceIframe';
@ -39,6 +45,10 @@ type ConfigureFormValues = ConfigureDeploymentFormValues &
const DEFAULT_MAX_PRICE = '10000'; const DEFAULT_MAX_PRICE = '10000';
const TX_APPROVAL_TIMEOUT_MS = 60000; const TX_APPROVAL_TIMEOUT_MS = 60000;
/**
* Configure component that allows users to configure their deployment.
* @returns {JSX.Element} - The Configure component.
*/
const Configure = () => { const Configure = () => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [deployers, setDeployers] = useState<Deployer[]>([]); const [deployers, setDeployers] = useState<Deployer[]>([]);
@ -106,6 +116,16 @@ const Configure = () => {
selectedNumProviders, 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 ( const createProject = async (
data: FieldValues, data: FieldValues,
envVariables: AddEnvironmentVariableInput[], 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 ( const verifyTx = async (
senderAddress: string, senderAddress: string,
txHash: string, txHash: string,
@ -199,6 +228,13 @@ const Configure = () => {
return isValid; return isValid;
}; };
/**
* Handles the form submission.
* @async
* @function handleFormSubmit
* @param {FieldValues} createFormData - The form data.
* @returns {Promise<void>}
*/
const handleFormSubmit = useCallback( const handleFormSubmit = useCallback(
async (createFormData: FieldValues) => { async (createFormData: FieldValues) => {
try { try {
@ -315,11 +351,23 @@ const Configure = () => {
[client, createProject, dismiss, toast, amountToBePaid], [client, createProject, dismiss, toast, amountToBePaid],
); );
/**
* Fetches the deployers using the GQLClient.
* @async
* @function fetchDeployers
* @returns {Promise<void>}
*/
const fetchDeployers = useCallback(async () => { const fetchDeployers = useCallback(async () => {
const res = await client.getDeployers(); const res = await client.getDeployers();
setDeployers(res.deployers); setDeployers(res.deployers);
}, [client]); }, [client]);
/**
* Handles the deployer change.
* @function onDeployerChange
* @param {string} selectedLrn - The selected LRN.
* @returns {void}
*/
const onDeployerChange = useCallback( const onDeployerChange = useCallback(
(selectedLrn: string) => { (selectedLrn: string) => {
const deployer = deployers.find((d) => d.deployerLrn === selectedLrn); const deployer = deployers.find((d) => d.deployerLrn === selectedLrn);
@ -328,6 +376,14 @@ const Configure = () => {
[deployers], [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( const cosmosSendTokensHandler = useCallback(
async (selectedAccount: string, amount: string) => { async (selectedAccount: string, amount: string) => {
if (!selectedAccount) { if (!selectedAccount) {
@ -399,6 +455,15 @@ const Configure = () => {
[client, dismiss, toast], [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 ( const requestTx = async (
sender: string, sender: string,
recipient: string, recipient: string,
@ -435,6 +500,11 @@ const Configure = () => {
} }
}, [isBalanceSufficient]); }, [isBalanceSufficient]);
/**
* Handles the configure deployment.
* @function handleConfigureDeployment
* @returns {void}
*/
const handleConfigureDeployment = useCallback(() => { const handleConfigureDeployment = useCallback(() => {
methods.handleSubmit(handleFormSubmit)(); methods.handleSubmit(handleFormSubmit)();
}, [handleFormSubmit, methods]); }, [handleFormSubmit, methods]);
@ -448,7 +518,7 @@ const Configure = () => {
</Heading> </Heading>
<Heading <Heading
as="h5" 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 The app can be deployed by setting the deployer LRN for a single
deployment or by creating a deployer auction for multiple deployment or by creating a deployer auction for multiple
@ -457,7 +527,7 @@ const Configure = () => {
</div> </div>
</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}> <FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(handleFormSubmit)}> <form onSubmit={methods.handleSubmit(handleFormSubmit)}>
<div className="flex flex-col justify-start gap-4 mb-6"> <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"> <div className="flex flex-col justify-start gap-4 mb-6">
<Heading <Heading
as="h5" 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 The app will be deployed by the configured deployer
</Heading> </Heading>
@ -500,14 +570,16 @@ const Configure = () => {
rules={{ required: true }} rules={{ required: true }}
render={({ field: { value, onChange }, fieldState }) => ( render={({ field: { value, onChange }, fieldState }) => (
<FormControl fullWidth error={Boolean(fieldState.error)}> <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 Select deployer LRN
</span> </span>
<Select <Select
value={value} value={value}
onChange={(event) => { onChange={(event) => {
onChange(event.target.value); onChange((event.target as HTMLInputElement).value);
onDeployerChange(event.target.value); onDeployerChange(
(event.target as HTMLInputElement).value,
);
}} }}
displayEmpty displayEmpty
size="small" size="small"
@ -538,12 +610,12 @@ const Configure = () => {
<div className="flex flex-col justify-start gap-4 mb-6"> <div className="flex flex-col justify-start gap-4 mb-6">
<Heading <Heading
as="h5" 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 Set the number of deployers and maximum price for each
deployment deployment
</Heading> </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 Number of Deployers
</span> </span>
<Controller <Controller
@ -553,15 +625,16 @@ const Configure = () => {
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<Input <Input
type="number" type="number"
value={value} value={value ?? ''}
onChange={(e) => onChange(e)} onChange={(e) =>
min={1} onChange((e.target as HTMLInputElement).value)
}
/> />
)} )}
/> />
</div> </div>
<div className="flex flex-col justify-start gap-4 mb-6"> <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) Maximum Price (alnt)
</span> </span>
<Controller <Controller
@ -569,41 +642,45 @@ const Configure = () => {
control={methods.control} control={methods.control}
rules={{ required: true }} rules={{ required: true }}
render={({ field: { value, onChange } }) => ( 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> </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 Environment Variables
</Heading> </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 /> <EnvironmentVariablesForm />
</div> </div>
{selectedOption === 'LRN' && !selectedDeployer?.minimumPayment ? ( {selectedOption === 'LRN' && !selectedDeployer?.minimumPayment ? (
<div> <div>
<Button <IconButton
{...buttonSize} {...buttonSize}
type="submit" type="submit"
title="Deploy"
disabled={isLoading || !selectedDeployer} disabled={isLoading || !selectedDeployer}
rightIcon={ isLoading={isLoading}
isLoading ? ( loadingText="Deploying"
<Loader2 className="ml-2 h-4 w-4 animate-spin" /> rightIcon={<ArrowRightCircle />}
) : ( />
<ArrowRightCircle className="ml-2 h-4 w-4" />
)
}
>
{isLoading ? 'Deploying' : 'Deploy'}
</Button>
</div> </div>
) : ( ) : (
<div className="flex gap-4"> <div className="flex gap-4">
<Button <IconButton
{...buttonSize} {...buttonSize}
type="submit"
title="Configure"
disabled={ disabled={
isLoading || isLoading ||
isPaymentLoading || isPaymentLoading ||
@ -612,33 +689,16 @@ const Configure = () => {
amountToBePaid === '' || amountToBePaid === '' ||
selectedNumProviders === '' selectedNumProviders === ''
} }
rightIcon={ isLoading={isLoading || isPaymentLoading}
isLoading || isPaymentLoading ? ( loadingText="Configuring"
<Loader2 className="ml-2 h-4 w-4 animate-spin" /> rightIcon={<ArrowRightCircle />}
) : (
<ArrowRightCircle className="ml-2 h-4 w-4" />
)
}
onClick={handleConfigureDeployment} 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 ? ( {isAccountsDataReceived && isBalanceSufficient !== undefined ? (
!selectedAccount || !isBalanceSufficient ? ( !selectedAccount || !isBalanceSufficient ? (
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button <Button
{...buttonSize} {...buttonSize}
shape="default"
onClick={(e: any) => { onClick={(e: any) => {
e.preventDefault(); e.preventDefault();
setBalanceMessage('Waiting for payment'); setBalanceMessage('Waiting for payment');
@ -651,7 +711,7 @@ const Configure = () => {
> >
Buy prepaid service Buy prepaid service
</Button> </Button>
<p className="text-gray-700 dark:text-gray-300"> <p className="dark:text-gray-300 text-gray-700">
{balanceMessage !== undefined ? ( {balanceMessage !== undefined ? (
<div className="flex items-center gap-2 text-white"> <div className="flex items-center gap-2 text-white">
<Loader2 className="animate-spin w-5 h-5" /> <Loader2 className="animate-spin w-5 h-5" />

View File

@ -2,14 +2,13 @@ import React from 'react';
import OauthPopup from 'react-oauth-popup'; import OauthPopup from 'react-oauth-popup';
import { useGQLClient } from '../../../context/GQLClientContext'; import { useGQLClient } from '../../../context/GQLClientContext';
import { Button } from '../../shared/Button';
import { Github, GitBranch, MoreHorizontal } from 'lucide-react'; import { IconButton } from '@/components/ui/extended/button-w-icons';
import { VITE_GITHUB_CLIENT_ID } from 'utils/constants'; import { GitBranch, Github, MoreHorizontal } from 'lucide-react';
import { VITE_GITHUB_CLIENT_ID } from '../../../utils/constants';
import { Heading } from '../../shared/Heading'; import { Heading } from '../../shared/Heading';
import { IconWithFrame } from '../../shared/IconWithFrame'; import { IconWithFrame } from '../../shared/IconWithFrame';
import { useToast } from '../../shared/Toast'; import { useToast } from '../../shared/Toast';
import { MockConnectGitCard } from './MockConnectGitCard';
const SCOPES = 'public_repo user'; const SCOPES = 'public_repo user';
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`; 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; 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> = ({ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
onAuth: onToken, onAuth: onToken,
}: ConnectAccountInterface) => { }: ConnectAccountInterface) => {
@ -25,6 +30,13 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
const { toast, dismiss } = useToast(); 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) => { const handleCode = async (code: string) => {
// Pass code to backend and get access token // Pass code to backend and get access token
const { const {
@ -41,10 +53,10 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
// TODO: Use correct height // TODO: Use correct height
return ( 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]"> <div className="flex flex-col items-center max-w-[420px]">
{/** Icons */} {/** 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} /> <IconWithFrame icon={<GitBranch />} hasHighlight={false} />
<MoreHorizontal className="items-center gap-1.5 flex" /> <MoreHorizontal className="items-center gap-1.5 flex" />
<IconWithFrame <IconWithFrame
@ -55,16 +67,16 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
</div> </div>
{/** Text */} {/** Text */}
<div className="flex flex-col gap-1.5 mb-6"> <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 Connect to your Git account
</Heading> </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 Once connected, you can import a repository from your account or
start with one of our templates. start with one of our templates.
</p> </p>
</div> </div>
{/** CTA Buttons */} {/** 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 <OauthPopup
url={GITHUB_OAUTH_URL} url={GITHUB_OAUTH_URL}
onCode={handleCode} onCode={handleCode}
@ -73,27 +85,27 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
width={1000} width={1000}
height={1000} height={1000}
> >
<Button <IconButton
className="w-full sm:w-auto" className="sm:w-auto w-full"
leftIcon={<Github />} leftIcon={<GitBranch />}
variant="primary" variant="default"
> >
Connect to GitHub Connect to GitHub
</Button> </IconButton>
</OauthPopup> </OauthPopup>
<Button <IconButton
className="w-full sm:w-auto" className="sm:w-auto w-full"
leftIcon={<Github />} leftIcon={<Github />}
variant="primary" variant="default"
> >
Connect to GitTea Connect to GitTea
</Button> </IconButton>
</div> </div>
</div> </div>
{/* TODO: Add ConnectAccountTabPanel */} {/* TODO: Add ConnectAccountTabPanel */}
<MockConnectGitCard /> {/* <MockConnectGitCard /> */}
{/* <div className="rounded-l shadow p-2 flex-col justify-start items-start gap-2 inline-flex"> {/* <div className="inline-flex flex-col items-start justify-start gap-2 p-2 rounded-l shadow">
<ConnectAccountTabPanel /> <ConnectAccountTabPanel />
</div> */} </div> */}
</div> </div>

View File

@ -1,7 +1,11 @@
import React from 'react'; 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 = () => { const ConnectAccountTabPanel: React.FC = () => {
return ( return (
<Tabs <Tabs

View File

@ -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 axios from 'axios';
import { Deployment } from 'gql-client'; 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 { 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 { 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; const FETCH_DEPLOYMENTS_INTERVAL = 5000;
type RequestState = type RequestState =
@ -30,6 +29,10 @@ type Record = {
logAvailable: boolean; logAvailable: boolean;
}; };
/**
* Deploy component that displays the deployment status of a project.
* @returns {JSX.Element} - The Deploy component.
*/
const Deploy = () => { const Deploy = () => {
const client = useGQLClient(); const client = useGQLClient();
@ -49,6 +52,10 @@ const Deploy = () => {
navigate(`/${orgSlug}/projects/create`); 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(() => { const isDeploymentFailed = useMemo(() => {
if (!record) { if (!record) {
return false; return false;
@ -62,6 +69,12 @@ const Deploy = () => {
} }
}, [record]); }, [record]);
/**
* Fetches the deployment records from the deployer API.
* @async
* @function fetchDeploymentRecords
* @returns {Promise<void>}
*/
const fetchDeploymentRecords = useCallback(async () => { const fetchDeploymentRecords = useCallback(async () => {
if (!deployment) { if (!deployment) {
return; return;
@ -79,6 +92,12 @@ const Deploy = () => {
} }
}, [deployment]); }, [deployment]);
/**
* Fetches the deployment information from the GQLClient.
* @async
* @function fetchDeployment
* @returns {Promise<void>}
*/
const fetchDeployment = useCallback(async () => { const fetchDeployment = useCallback(async () => {
if (!projectId) { if (!projectId) {
return; return;
@ -126,14 +145,14 @@ const Deploy = () => {
/> />
</div> </div>
</div> </div>
<Button <IconButton
onClick={handleOpen} onClick={handleOpen}
leftIcon={<AlertTriangle />}
aria-label="Cancel deployment"
size="sm" size="sm"
variant="tertiary"
leftIcon={<AlertTriangle size={16} />}
> >
Cancel Cancel
</Button> </IconButton>
<CancelDeploymentDialog <CancelDeploymentDialog
handleCancel={handleOpen} handleCancel={handleOpen}
open={open} open={open}

View File

@ -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 { Check, Clock, Loader2 } from 'lucide-react';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
enum DeployStatus { enum DeployStatus {
PROCESSING = 'progress', PROCESSING = 'progress',
@ -9,6 +9,9 @@ enum DeployStatus {
ERROR = 'error', ERROR = 'error',
} }
/**
* DeployStepsProps interface defines the props for the DeployStep component.
*/
interface DeployStepsProps { interface DeployStepsProps {
status: DeployStatus; status: DeployStatus;
title: string; title: string;
@ -17,6 +20,11 @@ interface DeployStepsProps {
processTime?: string; 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) => { const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
return ( return (
<div className="border-b border-border-separator"> <div className="border-b border-border-separator">
@ -64,10 +72,7 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
{status === DeployStatus.COMPLETE && ( {status === DeployStatus.COMPLETE && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<div className="w-4.5 h-4.5 grid place-content-center"> <div className="w-4.5 h-4.5 grid place-content-center">
<Check <Check className="text-elements-success" size={15} />
className="text-elements-success"
size={15}
/>
</div> </div>
</div> </div>
)} )}
@ -76,4 +81,4 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
); );
}; };
export { DeployStep, DeployStatus }; export { DeployStatus, DeployStep };

View File

@ -1,12 +1,15 @@
import React, { useMemo } from 'react'; import { SegmentedControls } from '@/components/shared/SegmentedControls';
import { SegmentedControls } from 'components/shared/SegmentedControls'; import React, { useMemo, useState } from 'react';
import { useState } from 'react';
import { useMediaQuery } from 'usehooks-ts'; import { useMediaQuery } from 'usehooks-ts';
import { Github, GitBranch, LayoutDashboard } from 'lucide-react'; import templates from '@/assets/templates';
import { relativeTimeISO } from 'utils/time'; import { relativeTimeISO } from '@/utils/time';
import templates from 'assets/templates'; 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 = () => { export const MockConnectGitCard = () => {
const [segmentedControlsValue, setSegmentedControlsValue] = const [segmentedControlsValue, setSegmentedControlsValue] =
useState<string>('import'); 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(() => { const renderContent = useMemo(() => {
if (segmentedControlsValue === 'import') { if (segmentedControlsValue === 'import') {
return ( 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) => ( {IMPORT_CONTENT.map((repo, index) => (
<React.Fragment key={index}> <React.Fragment key={index}>
<MockProjectCard {...repo} /> <MockProjectCard {...repo} />
{index !== IMPORT_CONTENT.length - 1 && ( {index !== IMPORT_CONTENT.length - 1 && (
<div className="border-b border-base-border" /> <div className="border-base-border border-b" />
)} )}
</React.Fragment> </React.Fragment>
))} ))}
@ -59,7 +67,7 @@ export const MockConnectGitCard = () => {
); );
} }
return ( 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) => ( {templates.map((template, index) => (
<MockTemplateCard key={index} {...template} /> <MockTemplateCard key={index} {...template} />
))} ))}
@ -80,11 +88,19 @@ export const MockConnectGitCard = () => {
{renderContent} {renderContent}
{/* Shade */} {/* 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> </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 = ({ const MockProjectCard = ({
full_name, full_name,
updated_at, updated_at,
@ -95,24 +111,24 @@ const MockProjectCard = ({
visibility?: string; visibility?: string;
}) => { }) => {
return ( 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 */} {/* Icon container */}
<div className="w-10 h-10 bg-base-bg dark:bg-background rounded-md justify-center items-center flex"> <div className="bg-base-bg dark:bg-background flex items-center justify-center w-10 h-10 rounded-md">
<Github className="h-5 w-5" /> <Github className="w-5 h-5" />
</div> </div>
{/* Content */} {/* 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"> <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]"> <p className="text-elements-high-em text-sm dark:text-foreground font-medium tracking-[-0.006em]">
{full_name} {full_name}
</p> </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)} {updated_at && relativeTimeISO(updated_at)}
</p> </p>
</div> </div>
{visibility === 'private' && ( {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"> <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="h-4 w-4" /> <GitBranch className="w-4 h-4" />
Private Private
</div> </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 }) => { const MockTemplateCard = ({ name }: { name: string }) => {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<LayoutDashboard className="h-4 w-4" /> <LayoutDashboard className="w-4 h-4" />
<p className="text-sm font-medium text-elements-primary">{name}</p> <p className="text-elements-primary text-sm font-medium">{name}</p>
</div> </div>
); );
}; };

View File

@ -3,17 +3,33 @@ import { useNavigate, useParams } from 'react-router-dom';
import { Spinner } from '@snowballtools/material-tailwind-react-fork'; import { Spinner } from '@snowballtools/material-tailwind-react-fork';
import { Button } from 'components/shared/Button'; import { useToast } from '@/components/shared/Toast';
import { useToast } from 'components/shared/Toast'; import { Button } from '@/components/ui';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from '@/context/GQLClientContext';
import { relativeTimeISO } from '@/utils/time';
import { ArrowRight, Github, Lock } from 'lucide-react'; import { ArrowRight, Github, Lock } from 'lucide-react';
import { relativeTimeISO } from 'utils/time';
import { GitRepositoryDetails } from '../../../../types/types'; import { GitRepositoryDetails } from '../../../../types/types';
/**
* Props for the ProjectRepoCard component.
*/
interface ProjectRepoCardProps { interface ProjectRepoCardProps {
/**
* The repository to display in the card.
*/
repository: GitRepositoryDetails; 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> = ({ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
repository, repository,
}) => { }) => {
@ -41,15 +57,15 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
return ( return (
<div <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} onClick={createProject}
> >
{/* Icon container */} {/* 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 /> <Github />
</div> </div>
{/* Content */} {/* 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"> <div className="flex flex-col gap-1">
<p className="text-elements-high-em dark:text-foreground text-sm font-medium tracking-[-0.006em]"> <p className="text-elements-high-em dark:text-foreground text-sm font-medium tracking-[-0.006em]">
{repository.full_name} {repository.full_name}
@ -59,7 +75,7 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
</p> </p>
</div> </div>
{repository.visibility === 'private' && ( {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 /> <Lock />
Private Private
</div> </div>
@ -67,13 +83,12 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
</div> </div>
{/* Right action */} {/* Right action */}
{isLoading ? ( {isLoading ? (
<Spinner className="h-4 w-4 absolute right-3" /> <Spinner className="right-3 absolute w-4 h-4" />
) : ( ) : (
<Button <Button
variant="tertiary" variant="secondary"
size="sm" size="sm"
iconOnly className="sm:group-hover:flex right-3 absolute hidden"
className="sm:group-hover:flex hidden absolute right-3"
> >
<ArrowRight /> <ArrowRight />
</Button> </Button>

View File

@ -1,22 +1,28 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import assert from 'assert'; import assert from 'assert';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDebounce } from 'usehooks-ts'; 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 { 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 DEFAULT_SEARCHED_REPO = '';
const REPOS_PER_PAGE = 5; 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 = () => { export const RepositoryList = () => {
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO); const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
const [selectedAccount, setSelectedAccount] = useState<SelectOption>(); const [selectedAccount, setSelectedAccount] = useState<SelectOption>();
@ -128,7 +134,7 @@ export const RepositoryList = () => {
return ( return (
<section className="space-y-3"> <section className="space-y-3">
{/* Dropdown and search */} {/* 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"> <div className="lg:basis-1/3 w-full">
<Select <Select
options={options} options={options}
@ -139,8 +145,8 @@ export const RepositoryList = () => {
onChange={(value) => setSelectedAccount(value as SelectOption)} onChange={(value) => setSelectedAccount(value as SelectOption)}
/> />
</div> </div>
<div className="basis-2/3 flex w-full flex-grow"> <div className="basis-2/3 flex flex-grow w-full">
<Input <IconInput
className="w-full" className="w-full"
value={searchedRepo} value={searchedRepo}
placeholder="Search for repository" placeholder="Search for repository"
@ -164,18 +170,18 @@ export const RepositoryList = () => {
))} ))}
</div> </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"> <p className="text-elements-high-em dark:text-foreground font-sans">
No repository found No repository found
</p> </p>
<Button <IconButton
variant="tertiary" variant="outline"
leftIcon={<RotateCw />} leftIcon={<RotateCw />}
size="sm" size="sm"
onClick={handleResetFilters} onClick={handleResetFilters}
> >
Reset filters Reset filters
</Button> </IconButton>
</div> </div>
)} )}
</section> </section>

View File

@ -1,10 +1,10 @@
import { Button } from 'components/shared/Button'; import { Tag } from '@/components/shared/Tag';
import { Tag } from 'components/shared/Tag'; import { useToast } from '@/components/shared/Toast';
import { useToast } from 'components/shared/Toast'; import { Button } from '@/components/ui';
import { cn } from '@/utils/classnames';
import { GitBranch } from 'lucide-react'; import { GitBranch } from 'lucide-react';
import React, { ComponentPropsWithoutRef, useCallback } from 'react'; import React, { ComponentPropsWithoutRef, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { cn } from 'utils/classnames';
export interface TemplateDetail { export interface TemplateDetail {
id: string; id: string;
@ -60,11 +60,11 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
onClick={handleClick} onClick={handleClick}
> >
{/* Icon */} {/* 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 /> <GitBranch />
</div> </div>
{/* Name */} {/* 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} {template.name}
</p> </p>
{template?.isComingSoon ? ( {template?.isComingSoon ? (
@ -73,10 +73,10 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
</Tag> </Tag>
) : ( ) : (
<Button <Button
variant="tertiary" variant="secondary"
size="sm" size="sm"
iconOnly // iconOnly
className="group-hover:flex hidden absolute right-3" className="group-hover:flex right-3 absolute hidden"
> >
<GitBranch /> <GitBranch />
</Button> </Button>

View File

@ -10,42 +10,76 @@ import { useCallback, useState } from 'react';
import { import {
Avatar, Avatar,
AvatarFallback, AvatarFallback,
Modal as Dialog, Button,
ModalContent as DialogContent, Dialog,
ModalFooter as DialogFooter, DialogContent,
ModalTitle as DialogTitle, DialogFooter,
DialogTitle,
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from 'components/shared'; } from '@/components/ui';
import { Button } from 'components/shared/Button'; import { Heading } from '@/components/shared/Heading';
import { GitBranch, Clock, CheckCircle, AlertTriangle } from 'lucide-react'; import { OverflownText } from '@/components/shared/OverflownText';
import { Heading } from 'components/shared/Heading'; import { Tag } from '@/components/shared/Tag';
import { OverflownText } from 'components/shared/OverflownText'; import { getInitials } from '@/utils/geInitials';
import { Tag } from 'components/shared/Tag'; import { relativeTimeMs } from '@/utils/time';
import { getInitials } from 'utils/geInitials'; import { AlertTriangle, CheckCircle, Clock, GitBranch } from 'lucide-react';
import { relativeTimeMs } from 'utils/time';
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants'; import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
import { formatAddress } from '../../../../utils/format'; import { formatAddress } from '../../../../utils/format';
import { DeploymentMenu } from './DeploymentMenu'; import { DeploymentMenu } from './DeploymentMenu';
interface DeployDetailsCardProps { /**
deployment: Deployment; * Type definition for the status colors.
currentDeployment: Deployment; */
onUpdate: () => Promise<void>; const STATUS_COLORS: Record<
project: Project; DeploymentStatus,
prodBranchDomains: Domain[]; 'attention' | 'positive' | 'negative' | 'neutral'
} > = {
const STATUS_COLORS: Record<DeploymentStatus, 'attention' | 'positive' | 'negative' | 'neutral'> = {
[DeploymentStatus.Building]: 'attention', [DeploymentStatus.Building]: 'attention',
[DeploymentStatus.Ready]: 'positive', [DeploymentStatus.Ready]: 'positive',
[DeploymentStatus.Error]: 'negative', [DeploymentStatus.Error]: 'negative',
[DeploymentStatus.Deleting]: 'neutral', [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 = ({ const DeploymentDetailsCard = ({
deployment, deployment,
currentDeployment, currentDeployment,
@ -102,7 +136,11 @@ const DeploymentDetailsCard = ({
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className={className} style={{ cursor: 'pointer' }} onClick={fetchDeploymentLogs}> <div
className={className}
style={{ cursor: 'pointer' }}
onClick={fetchDeploymentLogs}
>
<Tag <Tag
type={STATUS_COLORS[deployment.status]} type={STATUS_COLORS[deployment.status]}
size="xs" size="xs"
@ -123,13 +161,13 @@ const DeploymentDetailsCard = ({
); );
return ( 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 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"> <div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full">
{/* DEPLOYMENT URL */} {/* DEPLOYMENT URL */}
{deployment.url && ( {deployment.url && (
<Heading <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" as="h2"
> >
<OverflownText content={deployment.url}> <OverflownText content={deployment.url}>
@ -138,32 +176,30 @@ const DeploymentDetailsCard = ({
</Heading> </Heading>
)} )}
{deployment.deployer.deployerLrn && ( {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} Deployer LRN: {deployment.deployer.deployerLrn}
</span> </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 {deployment.environment === Environment.Production
? `Production ${deployment.isCurrent ? '(Current)' : ''}` ? `Production ${deployment.isCurrent ? '(Current)' : ''}`
: 'Preview'} : 'Preview'}
</span> </span>
</div> </div>
</div> </div>
{/* DEPLOYMENT STATUS */} {/* DEPLOYMENT STATUS */}
{renderDeploymentStatus('w-[10%] max-w-[110px] hidden md:flex h-fit')} {renderDeploymentStatus('w-[10%] max-w-[110px] hidden md:flex h-fit')}
{/* DEPLOYMENT COMMIT DETAILS */} {/* DEPLOYMENT COMMIT DETAILS */}
<div className="flex w-full justify-between md:w-[25%]"> <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"> <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"> <span className="flex gap-1.5 items-center">
<GitBranch className="h-4 w-4" /> <GitBranch className="w-4 h-4" />
<OverflownText content={deployment.branch}> <OverflownText content={deployment.branch}>
{deployment.branch} {deployment.branch}
</OverflownText> </OverflownText>
</span> </span>
<span className="flex w-full gap-2 items-center"> <span className="flex items-center w-full gap-2">
<div className="h-4 w-4" /> <div className="w-4 h-4" />
<OverflownText content={deployment.commitMessage}> <OverflownText content={deployment.commitMessage}>
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '} {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
{deployment.commitMessage} {deployment.commitMessage}
@ -172,29 +208,32 @@ const DeploymentDetailsCard = ({
</div> </div>
{renderDeploymentStatus('flex md:hidden h-fit')} {renderDeploymentStatus('flex md:hidden h-fit')}
</div> </div>
{/* DEPLOYMENT INFOs */} {/* 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="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 md:w-[70%] xl:items-center gap-2 flex-1 xl:flex-row md:flex-col">
<div className="flex gap-2 items-center"> <div className="flex items-center gap-2">
<Clock className="h-4 w-4" /> <Clock className="w-4 h-4" />
<OverflownText content={relativeTimeMs(deployment.createdAt) ?? ''}> <OverflownText content={relativeTimeMs(deployment.createdAt) ?? ''}>
{relativeTimeMs(deployment.createdAt)} {relativeTimeMs(deployment.createdAt)}
</OverflownText> </OverflownText>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex items-center gap-2">
<div> <div>
<Avatar className="lg:size-5 2xl:size-6"> <Avatar className="lg:size-5 2xl:size-6">
<AvatarFallback>{getInitials(deployment.createdBy.name ?? '')}</AvatarFallback> <AvatarFallback>
{getInitials(deployment.createdBy.name ?? '')}
</AvatarFallback>
</Avatar> </Avatar>
</div> </div>
<OverflownText content={formatAddress(deployment.createdBy?.name ?? '')}> <OverflownText
content={formatAddress(deployment.createdBy?.name ?? '')}
>
{formatAddress(deployment.createdBy.name ?? '')} {formatAddress(deployment.createdBy.name ?? '')}
</OverflownText> </OverflownText>
</div> </div>
</div> </div>
<DeploymentMenu <DeploymentMenu
className="ml-auto md:static absolute top-4 right-0" className="md:static top-4 absolute right-0 ml-auto"
deployment={deployment} deployment={deployment}
currentDeployment={currentDeployment} currentDeployment={currentDeployment}
onUpdate={onUpdate} onUpdate={onUpdate}
@ -202,6 +241,7 @@ const DeploymentDetailsCard = ({
prodBranchDomains={prodBranchDomains} prodBranchDomains={prodBranchDomains}
/> />
</div> </div>
{/* @ts-ignore */}
<Dialog open={openDialog} onClose={handleCloseDialog}> <Dialog open={openDialog} onClose={handleCloseDialog}>
<DialogTitle>Deployment logs</DialogTitle> <DialogTitle>Deployment logs</DialogTitle>
<DialogContent className="bg-[rgba(0,0,0,0.9)] p-8 rounded-lg mx-2 text-gray-400 text-sm"> <DialogContent className="bg-[rgba(0,0,0,0.9)] p-8 rounded-lg mx-2 text-gray-400 text-sm">

View File

@ -1,16 +1,12 @@
import { Deployment } from 'gql-client'; 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 { 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 { interface DeploymentDialogBodyCardProps {
deployment: Deployment; deployment: Deployment;

View File

@ -7,11 +7,12 @@ import {
MenuList, MenuList,
} from '@snowballtools/material-tailwind-react-fork'; } from '@snowballtools/material-tailwind-react-fork';
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog'; import { ChangeStateToProductionDialog } from '@/components/projects/Dialog/ChangeStateToProductionDialog';
import { DeleteDeploymentDialog } from 'components/projects/Dialog/DeleteDeploymentDialog'; import { DeleteDeploymentDialog } from '@/components/projects/Dialog/DeleteDeploymentDialog';
import { Button } from 'components/shared/Button'; import { useToast } from '@/components/shared/Toast';
import { useToast } from 'components/shared/Toast'; import { Button } from '@/components/ui';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from '@/context/GQLClientContext';
import { cn } from '@/utils/classnames';
import { Deployment, Domain, Environment, Project } from 'gql-client'; import { Deployment, Domain, Environment, Project } from 'gql-client';
import { import {
Link, Link,
@ -19,18 +20,46 @@ import {
Rocket, Rocket,
RotateCw, RotateCw,
Trash2, Trash2,
Undo Undo,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from 'utils/classnames';
/**
* Props for the DeploymentMenu component.
*/
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> { interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
/**
* The deployment to display the menu for.
*/
deployment: Deployment; deployment: Deployment;
/**
* The current deployment.
*/
currentDeployment: Deployment; currentDeployment: Deployment;
/**
* Callback function to update deployments.
*/
onUpdate: () => Promise<void>; onUpdate: () => Promise<void>;
/**
* The project the deployment belongs to.
*/
project: Project; project: Project;
/**
* The domains for the production branch of the project.
*/
prodBranchDomains: Domain[]; 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 = ({ export const DeploymentMenu = ({
deployment, deployment,
currentDeployment, currentDeployment,
@ -150,9 +179,7 @@ export const DeploymentMenu = ({
<Menu placement="bottom-start"> <Menu placement="bottom-start">
<MenuHandler> <MenuHandler>
<Button <Button
shape="default" variant="ghost"
size="xs"
variant="unstyled"
className={cn( className={cn(
'h-8 w-8 rounded-full border border-transparent transition-colors background-transparent', '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', '[&[aria-expanded=true]]:border [&[aria-expanded=true]]:border-border-interactive [&[aria-expanded=true]]:bg-controls-tertiary [&[aria-expanded=true]]:shadow-button',

View File

@ -1,18 +1,20 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { DatePicker } from 'components/shared/DatePicker'; import { Select, SelectOption } from '@/components/shared/Select';
import { Input } from 'components/shared/Input'; import { IconInput } from '@/components/ui';
import { Select, SelectOption } from 'components/shared/Select'; import { Value } from '@/types';
import { import {
AlertTriangle, AlertTriangle,
CheckCircle, CheckCircle,
Loader2, Loader2,
Search, Search,
TrendingUp, TrendingUp,
X
} from 'lucide-react'; } from 'lucide-react';
import { Value } from 'types/vendor'; import { DatePicker } from '../../../shared/DatePicker';
/**
* Enum for the status options.
*/
export enum StatusOptions { export enum StatusOptions {
ALL_STATUS = 'All status', ALL_STATUS = 'All status',
BUILDING = 'Building', BUILDING = 'Building',
@ -20,19 +22,37 @@ export enum StatusOptions {
ERROR = 'Error', ERROR = 'Error',
} }
/**
* Interface for the filter value.
*/
export interface FilterValue { export interface FilterValue {
searchedBranch: string; searchedBranch: string;
status: StatusOptions | string; status: StatusOptions | string;
updateAtRange?: Value; updateAtRange?: Value;
} }
/**
* Interface for the filter form props.
*/
interface FilterFormProps { interface FilterFormProps {
value: FilterValue; value: FilterValue;
onChange: (value: FilterValue) => void; 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 FilterForm = ({ value, onChange }: FilterFormProps) => {
const [searchedBranch, setSearchedBranch] = useState(value.searchedBranch); const [searchedBranch, setSearchedBranch] = useState<string>(
value.searchedBranch || '',
);
const [selectedStatus, setSelectedStatus] = useState(value.status); const [selectedStatus, setSelectedStatus] = useState(value.status);
const [dateRange, setDateRange] = useState<Value>(); const [dateRange, setDateRange] = useState<Value>();
@ -70,24 +90,23 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
leftIcon: getOptionIcon(status), leftIcon: getOptionIcon(status),
})); }));
const handleReset = () => { // TODO handle reset with Icon button
setSearchedBranch(''); // const handleReset = () => {
}; // setSearchedBranch('');
// };
return ( return (
<div className="xl:grid xl:grid-cols-8 flex flex-col xl:gap-3 gap-3"> <div className="xl:grid xl:grid-cols-8 xl:gap-3 flex flex-col gap-3">
<div className="col-span-4 flex items-center"> <div className="flex items-center col-span-4">
<Input <IconInput
placeholder="Search branches" placeholder="Search branches"
leftIcon={<Search />} leftIcon={<Search />}
rightIcon={ // rightIcon={searchedBranch?.length > 0 ? <X onClick={handleReset} /> : undefined}
searchedBranch && <X onClick={handleReset} />
}
value={searchedBranch} value={searchedBranch}
onChange={(e) => setSearchedBranch(e.target.value)} onChange={(e) => setSearchedBranch(e.target.value)}
/> />
</div> </div>
<div className="col-span-2 flex items-center"> <div className="flex items-center col-span-2">
<DatePicker <DatePicker
className="w-full" className="w-full"
selectRange selectRange
@ -96,7 +115,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
onReset={() => setDateRange(undefined)} onReset={() => setDateRange(undefined)}
/> />
</div> </div>
<div className="col-span-2 flex items-center"> <div className="flex items-center col-span-2">
<Select <Select
leftIcon={getOptionIcon(selectedStatus as StatusOptions)} leftIcon={getOptionIcon(selectedStatus as StatusOptions)}
options={statusOptions} options={statusOptions}

View File

@ -1,8 +1,8 @@
import { GitCommitWithBranch } from '../../../../../types/types'; import { Heading } from '@/components/shared/Heading';
import { Heading } from 'components/shared/Heading'; import { Button } from '@/components/ui';
import ActivityCard from './ActivityCard';
import { Button } from 'components/shared/Button';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { GitCommitWithBranch } from '../../../../../types/types';
import ActivityCard from './ActivityCard';
export const Activity = ({ export const Activity = ({
isLoading, isLoading,
@ -12,16 +12,16 @@ export const Activity = ({
activities: GitCommitWithBranch[]; activities: GitCommitWithBranch[];
}) => { }) => {
return ( 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"> <div className="flex items-center justify-between">
<Heading className="text-lg leading-6 font-medium">Activity</Heading> <Heading className="text-lg font-medium leading-6">Activity</Heading>
<Button variant="tertiary" size="sm"> <Button variant="secondary" size="sm">
SEE ALL SEE ALL
</Button> </Button>
</div> </div>
<div className="mt-5"> <div className="mt-5">
{isLoading ? ( {isLoading ? (
<div className="grid place-content-center mt-10"> <div className="place-content-center grid mt-10">
<Loader className="animate-spin" /> <Loader className="animate-spin" />
</div> </div>
) : ( ) : (

View File

@ -1,10 +1,10 @@
import { Link } from 'react-router-dom'; import { Avatar } from '@/components/shared/Avatar';
import { GitCommitWithBranch } from '../../../../../types/types'; import { IconButton } from '@/components/ui';
import { Avatar } from 'components/shared/Avatar'; import { GitCommitWithBranch } from '@/types';
import { Button } from 'components/shared/Button'; import { getInitials } from '@/utils/geInitials';
import { Check, GitBranch } from 'lucide-react';
import { formatDistance } from 'date-fns'; import { formatDistance } from 'date-fns';
import { getInitials } from 'utils/geInitials'; import { Check, GitBranch } from 'lucide-react';
import { Link } from 'react-router-dom';
interface ActivityCardProps { interface ActivityCardProps {
activity: GitCommitWithBranch; activity: GitCommitWithBranch;
@ -24,7 +24,7 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
<Link <Link
to={activity.html_url} to={activity.html_url}
target="_blank" 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> <div>
<Avatar <Avatar
@ -38,7 +38,7 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
<span className="text-elements-high-em text-sm font-medium tracking-tight"> <span className="text-elements-high-em text-sm font-medium tracking-tight">
{activity.commit.author?.name} {activity.commit.author?.name}
</span> </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"> <span title={formattedDate} className="whitespace-nowrap">
{formattedDate} {formattedDate}
</span> </span>
@ -51,7 +51,7 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
<span <span
title={activity.branch.name} title={activity.branch.name}
// pseudo to increase hover area // 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 className="line-clamp-1">{activity.branch.name}</span>
</span> </span>
@ -59,17 +59,16 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
</span> </span>
<p <p
title={activity.commit.message} 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} {activity.commit.message}
</p> </p>
</div> </div>
<Button <IconButton
aria-label="Go to commit" aria-label="Go to commit"
variant="unstyled"
tabIndex={-1} tabIndex={-1}
className="p-0 text-elements-low-em group-focus-within:opacity-100 group-hover:opacity-100 opacity-0 transition-all" className="text-elements-low-em group-focus-within:opacity-100 group-hover:opacity-100 p-0 transition-all opacity-0"
leftIcon={<Check className="w-5 h-5" />} leftIcon={<Check />}
/> />
</Link> </Link>
{/* Separator calc => 100% - 36px (avatar) - 12px (padding-left) - 8px (gap) {/* Separator calc => 100% - 36px (avatar) - 12px (padding-left) - 8px (gap)

View File

@ -2,15 +2,17 @@ import { Auction, Deployer, Project } from 'gql-client';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { import {
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
} from '@mui/material'; } 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 { CheckCircle, Loader2 } from 'lucide-react';
import { useGQLClient } from 'context/GQLClientContext';
const WAIT_DURATION = 5000; const WAIT_DURATION = 5000;
@ -82,9 +84,9 @@ export const AuctionCard = ({ project }: { project: Project }) => {
return ( 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="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 justify-between items-center"> <div className="flex items-center justify-between">
<Heading className="text-lg leading-6 font-medium"> <Heading className="text-lg font-medium leading-6">
Auction details Auction details
</Heading> </Heading>
<Button onClick={handleOpenDialog} variant="secondary" size="sm"> <Button onClick={handleOpenDialog} variant="secondary" size="sm">
@ -92,7 +94,7 @@ export const AuctionCard = ({ project }: { project: Project }) => {
</Button> </Button>
</div> </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"> <span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Auction Id Auction Id
</span> </span>
@ -101,7 +103,7 @@ export const AuctionCard = ({ project }: { project: Project }) => {
</span> </span>
</div> </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"> <span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Auction Status Auction Status
</span> </span>
@ -124,7 +126,7 @@ export const AuctionCard = ({ project }: { project: Project }) => {
</p> </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"> <span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Deployer Funds Status Deployer Funds Status
</span> </span>
@ -169,9 +171,7 @@ export const AuctionCard = ({ project }: { project: Project }) => {
)} )}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleCloseDialog}> <Button onClick={handleCloseDialog}>Close</Button>
Close
</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</> </>

View File

@ -1,5 +1,5 @@
import { cloneElement } from '@/utils/cloneElement';
import { PropsWithChildren } from 'react'; import { PropsWithChildren } from 'react';
import { cloneElement } from 'utils/cloneElement';
interface OverviewInfoProps { interface OverviewInfoProps {
label: string; label: string;

View File

@ -1,9 +1,9 @@
import { UseFormRegister } from 'react-hook-form'; import { UseFormRegister } from 'react-hook-form';
import { EnvironmentVariablesFormValues } from '../../../../types'; import { Button } from '@/components/ui/button';
import { Button } from 'components/shared/Button'; import { Input } from '@/components/ui/input';
import { Trash } from 'lucide-react'; import { Trash } from 'lucide-react';
import { Input } from 'components/shared/Input'; import { EnvironmentVariablesFormValues } from '../../../../types';
interface AddEnvironmentVariableRowProps { interface AddEnvironmentVariableRowProps {
onDelete: () => void; onDelete: () => void;
@ -19,22 +19,20 @@ const AddEnvironmentVariableRow = ({
isDeleteDisabled, isDeleteDisabled,
}: AddEnvironmentVariableRowProps) => { }: AddEnvironmentVariableRowProps) => {
return ( return (
<div className="flex gap-2 py-1 self-stretch items-end"> <div className="flex items-end self-stretch gap-2 py-1">
<Input <Input
size="md"
{...register(`variables.${index}.key`, { {...register(`variables.${index}.key`, {
required: 'Key field cannot be empty', required: 'Key field cannot be empty',
})} })}
label={index === 0 ? 'Key' : undefined} placeholder={index === 0 ? 'Key' : undefined}
/> />
<Input <Input
size="md" placeholder={index === 0 ? 'Value' : undefined}
label={index === 0 ? 'Value' : undefined}
{...register(`variables.${index}.value`, { {...register(`variables.${index}.value`, {
required: 'Value field cannot be empty', required: 'Value field cannot be empty',
})} })}
/> />
<Button size="md" iconOnly onClick={onDelete} disabled={isDeleteDisabled}> <Button size="icon" onClick={onDelete} disabled={isDeleteDisabled}>
<Trash /> <Trash />
</Button> </Button>
</div> </div>

View File

@ -3,10 +3,10 @@ import { useForm } from 'react-hook-form';
import { Typography } from '@snowballtools/material-tailwind-react-fork'; import { Typography } from '@snowballtools/material-tailwind-react-fork';
import { Button } from 'components/shared/Button'; import { Modal } from '@/components/shared/Modal';
import { Modal } from 'components/shared/Modal'; import { Select, SelectOption } from '@/components/shared/Select';
import { Input } from 'components/shared/Input'; import { Button } from '@/components/ui/button';
import { Select, SelectOption } from 'components/shared/Select'; import { Input } from '@/components/ui/input';
import { AddProjectMemberInput, Permission } from 'gql-client'; import { AddProjectMemberInput, Permission } from 'gql-client';
interface AddMemberDialogProp { interface AddMemberDialogProp {
@ -93,10 +93,10 @@ const AddMemberDialog = ({
/> />
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button onClick={handleOpen} variant="danger" shape="default"> <Button onClick={handleOpen} variant="destructive">
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={!isValid} shape="default"> <Button type="submit" disabled={!isValid}>
SEND INVITE SEND INVITE
</Button> </Button>
</Modal.Footer> </Modal.Footer>

View File

@ -1,12 +1,12 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import { useGQLClient } from 'context/GQLClientContext'; import { Modal } from '@/components/shared/Modal';
import { useToast } from 'components/shared/Toast'; import { useToast } from '@/components/shared/Toast';
import { Modal } from 'components/shared/Modal'; import { Button, Input } from '@/components/ui';
import { Button } from 'components/shared/Button'; import { useGQLClient } from '@/context';
import { Input } from 'components/shared/Input'; import { FormHelperText } from '@mui/material';
import { Project } from 'gql-client'; import { Project } from 'gql-client';
interface DeleteProjectDialogProp { interface DeleteProjectDialogProp {
@ -59,7 +59,7 @@ const DeleteProjectDialog = ({
<form onSubmit={handleSubmit(deleteProjectHandler)}> <form onSubmit={handleSubmit(deleteProjectHandler)}>
<Modal.Body> <Modal.Body>
<Input <Input
label={ title={
"Deleting your project is irreversible. Enter your project's name " + "Deleting your project is irreversible. Enter your project's name " +
'"' + '"' +
project.name + project.name +
@ -71,14 +71,17 @@ const DeleteProjectDialog = ({
required: 'Project name is required', required: 'Project name is required',
validate: (value) => value === project.name, 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.Body>
<Modal.Footer className="flex justify-start"> <Modal.Footer className="flex justify-start">
<Button onClick={handleOpen} variant="tertiary"> <Button onClick={handleOpen} variant="secondary">
Cancel Cancel
</Button> </Button>
<Button variant="danger" type="submit" disabled={!isValid}> <Button variant="destructive" type="submit" disabled={!isValid}>
Yes, delete project Yes, delete project
</Button> </Button>
</Modal.Footer> </Modal.Footer>

Some files were not shown because too many files have changed in this diff Show More