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

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/lodash": "^4.17.0",
"@types/luxon": "^3.3.7",
"@types/node": "^16.18.68",
"@types/node": "^22.13.5",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/uuid": "^9.0.8",

View File

@ -1,37 +1,32 @@
import { ThemeProvider } from 'next-themes';
import { useEffect } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { BASE_URL } from 'utils/constants';
import ProjectSearchLayout from './layouts/ProjectSearch';
import { DashboardLayout } from './layouts/DashboardLayout';
import Index from './pages';
import AuthPage from './pages/AuthPage';
import BuyPrepaidService from './pages/BuyPrepaidService';
import Projects from './pages/org-slug';
import { projectsRoutesWithoutSearch } from './pages/org-slug/projects/project-routes';
import Settings from './pages/org-slug/Settings';
import { DashboardLayout } from './pages/org-slug/layout';
import {
projectsRoutesWithoutSearch,
projectsRoutesWithSearch,
} from './pages/org-slug/projects/routes';
import { BASE_URL } from './utils/constants';
const router = createBrowserRouter([
{
path: ':orgSlug',
element: <DashboardLayout />,
children: [
{
element: <ProjectSearchLayout />,
children: [
{
path: '',
element: <Projects />,
},
{
path: 'projects',
children: projectsRoutesWithSearch,
},
],
},
// {
// element: <ProjectSearchLayout />,
// children: [
// {
// path: '',
// element: <Projects />,
// },
// {
// path: 'projects',
// children: projectsRoutesWithSearch,
// },
// ],
// },
{
path: 'settings',
element: <Settings />,
@ -56,6 +51,11 @@ const router = createBrowserRouter([
},
]);
/**
* Main application component.
* Sets up routing, authentication, and theme provider.
* @returns {JSX.Element} The rendered application.
*/
function App() {
// Hacky way of checking session
// TODO: Handle redirect backs
@ -66,7 +66,6 @@ function App() {
const path = window.location.pathname;
if (res.status !== 200) {
localStorage.clear();
if (path !== '/login') {
window.location.pathname = '/login';
}
@ -78,7 +77,11 @@ function App() {
});
}, []);
return <RouterProvider router={router} />;
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<RouterProvider router={router} fallbackElement={<div>Loading...</div>} />
</ThemeProvider>
);
}
export default App;

View File

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

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

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 {
className?: string
width?: number
height?: number
/**
* Optional CSS class names to apply to the component.
*/
className?: string;
/**
* The width of the icon.
* @default 40
*/
width?: number;
/**
* The height of the icon.
* @default 40
*/
height?: number;
}
export const LaconicIcon: React.FC<LaconicIconProps> = ({ className = "", width = 40, height = 40 }) => {
/**
* A component that renders the Laconic icon.
*
* @param {LaconicIconProps} props - The props for the component.
* @returns {React.ReactElement} An SVG element representing the Laconic icon.
*/
export const LaconicIcon: React.FC<LaconicIconProps> = ({
className = '',
width = 40,
height = 40,
}) => {
return (
<svg
width={width}
@ -23,6 +47,5 @@ export const LaconicIcon: React.FC<LaconicIconProps> = ({ className = "", width
className="fill-current"
/>
</svg>
)
}
);
};

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';
/**
* WalletSessionIdProps interface defines the props for the WalletSessionId component.
*/
interface WalletSessionIdProps {
/**
* The wallet ID to display.
*/
walletId?: string;
/**
* Optional CSS class names to apply to the component.
*/
className?: string;
}
/**
* A component that displays the wallet session ID.
*
* @param {WalletSessionIdProps} props - The props for the component.
* @returns {React.ReactElement} A div element containing the wallet session ID.
*/
export const WalletSessionId: React.FC<WalletSessionIdProps> = ({
walletId,
className = '',
}) => {
// const { wallet } = useWallet();
const wallet = {id: 'x123xxx'}
const wallet = { id: 'x123xxx' };
const displayId = walletId || wallet?.id || 'Not Connected';
return (

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
} from '@/components/shared/ConfirmDialog';
interface DeleteVariableDialogProps extends ConfirmDialogProps {
variableKey: string;
@ -21,9 +21,9 @@ export const DeleteVariableDialog = ({
open={open}
confirmButtonTitle="Yes, confirm delete"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
confirmButtonProps={{ variant: 'destructive' }}
>
<p className="text-sm text-elements-mid-em">
<p className="text-elements-mid-em text-sm">
Are you sure you want to delete the variable{' '}
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
{variableKey}

View File

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

View File

@ -1,6 +1,6 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
} from '@/components/shared/ConfirmDialog';
interface DisconnectRepositoryDialogProps extends ConfirmDialogProps {}
@ -18,9 +18,9 @@ export const DisconnectRepositoryDialog = ({
open={open}
confirmButtonTitle="Yes, confirm disconnect"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
confirmButtonProps={{ variant: 'destructive' }}
>
<p className="text-sm text-elements-high-em">
<p className="text-elements-high-em text-sm">
Any data tied to your Git project may become misconfigured. Are you sure
you want to continue?
</p>

View File

@ -1,8 +1,8 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
} from '@/components/shared/ConfirmDialog';
import { formatAddress } from 'utils/format';
import { formatAddress } from '@/utils/format';
interface RemoveMemberDialogProps extends ConfirmDialogProps {
memberName: string;
@ -27,9 +27,9 @@ export const RemoveMemberDialog = ({
open={open}
confirmButtonTitle="Yes, remove member"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
confirmButtonProps={{ variant: 'destructive' }}
>
<p className="text-sm text-elements-high-em">
<p className="text-elements-high-em text-sm">
Once removed, {formatAddress(memberName)} ({formatAddress(ethAddress)}@
{emailDomain}) will not be able to access this project.
</p>

View File

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

View File

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

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 { Button } from 'components/shared/Button';
import { X, Search } from 'lucide-react';
import { Input } from 'components/shared/Input';
import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client';
import { useDebounceValue } from 'usehooks-ts';
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
import { useNavigate } from 'react-router-dom';
import { useCombobox } from 'downshift';
import { Project } from 'gql-client';
import { Search, X } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDebounceValue } from 'usehooks-ts';
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
interface ProjectSearchBarDialogProps extends Dialog.DialogProps {
open?: boolean;
@ -26,6 +26,7 @@ export const ProjectSearchBarDialog = ({
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
const client = useGQLClient();
const navigate = useNavigate();
const inputRef = useRef<HTMLInputElement>(null);
const {
getInputProps,
@ -75,22 +76,32 @@ export const ProjectSearchBarDialog = ({
return (
<Dialog.Root {...props}>
<Dialog.Portal>
<Dialog.Overlay className="bg-base-bg fixed inset-0 md:hidden overflow-y-auto" />
<Dialog.Overlay className="bg-base-bg md:hidden fixed inset-0 overflow-y-auto" />
<Dialog.Content>
<div className="h-full flex flex-col fixed top-0 inset-0">
<div className="fixed inset-0 top-0 flex flex-col h-full">
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
<div className="relative flex-1">
<Search className="left-2 top-1/2 text-muted-foreground absolute w-4 h-4 -translate-y-1/2" />
<Input
{...getInputProps({}, { suppressRefError: true })}
leftIcon={<Search />}
{...getInputProps(
{ ref: inputRef },
{ suppressRefError: true },
)}
className="pl-8"
placeholder="Search"
appearance="borderless"
autoFocus
type="text"
/>
<Button iconOnly variant="ghost" onClick={handleClose}>
<X size={16} />
</div>
<Button
variant="ghost"
size="icon"
onClick={handleClose}
type="button"
>
<X className="w-4 h-4" />
</Button>
</div>
{/* Content */}
<div
className="flex flex-col gap-1 px-2 py-2"
{...getMenuProps(

View File

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

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 { Project } from 'gql-client';
import { ComponentPropsWithoutRef, forwardRef } from 'react';
import { OmitCommon } from 'types/common';
import { cn } from 'utils/classnames';
import { getInitials } from 'utils/geInitials';
/**
* Represents a type that merges ComponentPropsWithoutRef<'li'> with certain exclusions.

View File

@ -1,10 +1,20 @@
import { Modal, ModalContent } from 'components/shared';
import { useCallback, useEffect } from 'react';
import { Modal } from '@/components/shared/Modal';
import { ModalContent } from '@/components/shared/Modal/ModalContent';
// import { Modal } from '@mui/material';
import {
VITE_LACONICD_CHAIN_ID,
VITE_WALLET_IFRAME_URL,
} from 'utils/constants';
} from '@/utils/constants';
import { useCallback, useEffect } from 'react';
/**
* ApproveTransactionModal component that displays a modal with an iframe to approve transactions.
* @param {Object} props - The component props.
* @param {function} props.setAccount - Callback function to set the account.
* @param {function} props.setIsDataReceived - Callback function to set whether the data is received.
* @param {boolean} props.isVisible - Determines whether the modal is visible.
* @returns {JSX.Element} - The ApproveTransactionModal component.
*/
const ApproveTransactionModal = ({
setAccount,
setIsDataReceived,
@ -22,7 +32,9 @@ const ApproveTransactionModal = ({
setIsDataReceived(true);
if (event.data.data.length === 0) {
console.error(`Accounts not present for chainId: ${VITE_LACONICD_CHAIN_ID}`);
console.error(
`Accounts not present for chainId: ${VITE_LACONICD_CHAIN_ID}`,
);
return;
}
@ -41,6 +53,11 @@ const ApproveTransactionModal = ({
};
}, []);
/**
* Gets the data from the wallet.
* @function getDataFromWallet
* @returns {void}
*/
const getDataFromWallet = useCallback(() => {
const iframe = document.getElementById('walletIframe') as HTMLIFrameElement;
@ -59,7 +76,7 @@ const ApproveTransactionModal = ({
}, []);
return (
<Modal open={isVisible} onClose={() => {}}>
<Modal open={isVisible}>
<ModalContent className="fixed left-1/2 top-1/2 w-[90%] max-w-[1200px] -translate-x-1/2 -translate-y-1/2 overflow-auto rounded-lg border bg-background shadow-lg">
<iframe
onLoad={getDataFromWallet}

View File

@ -2,12 +2,20 @@ import { useEffect, useState } from 'react';
import { Modal } from '@mui/material';
import { VITE_WALLET_IFRAME_URL } from 'utils/constants';
import useCheckBalance from '../../../hooks/useCheckBalance';
import { VITE_WALLET_IFRAME_URL } from '@/utils/constants';
import useCheckBalance from '@/hooks/useCheckBalance';
const CHECK_BALANCE_INTERVAL = 5000;
const IFRAME_ID = 'checkBalanceIframe';
/**
* CheckBalanceIframe component that checks the balance using an iframe.
* @param {Object} props - The component props.
* @param {function} props.onBalanceChange - Callback function to be called when the balance changes.
* @param {boolean} props.isPollingEnabled - Determines whether to poll the balance periodically.
* @param {string} props.amount - The amount to check against the balance.
* @returns {JSX.Element} - The CheckBalanceIframe component.
*/
const CheckBalanceIframe = ({
onBalanceChange,
isPollingEnabled,
@ -24,6 +32,9 @@ const CheckBalanceIframe = ({
const [isLoaded, setIsLoaded] = useState(false);
/**
* useEffect hook that calls checkBalance when the component is loaded or the amount changes.
*/
useEffect(() => {
if (!isLoaded) {
return;
@ -31,6 +42,10 @@ const CheckBalanceIframe = ({
checkBalance();
}, [amount, checkBalance, isLoaded]);
/**
* useEffect hook that sets up an interval to poll the balance if polling is enabled.
* Clears the interval when the component unmounts, balance is sufficient, or polling is disabled.
*/
useEffect(() => {
if (!isPollingEnabled || !isLoaded || isBalanceSufficient) {
return;
@ -45,9 +60,12 @@ const CheckBalanceIframe = ({
};
}, [isBalanceSufficient, isPollingEnabled, checkBalance, isLoaded]);
/**
* useEffect hook that calls the onBalanceChange callback when the isBalanceSufficient state changes.
*/
useEffect(() => {
onBalanceChange(isBalanceSufficient);
}, [isBalanceSufficient]);
}, [isBalanceSufficient, onBalanceChange]);
return (
<Modal open={false} disableEscapeKeyDown keepMounted>

View File

@ -5,23 +5,29 @@ import {
Deployer,
} from 'gql-client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Controller, FieldValues, FormProvider, useForm } from 'react-hook-form';
import {
Controller,
FieldValues,
FormProvider,
useForm,
} from 'react-hook-form';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useMediaQuery } from 'usehooks-ts';
import { FormControl, FormHelperText, MenuItem, Select } from '@mui/material';
import { useToast } from '@/components/shared/Toast';
import { Button } from '@/components/ui/button';
import { IconButton } from '@/components/ui/extended/button-w-icons';
import { Input } from '@/components/ui/input';
import EnvironmentVariablesForm from '@/pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
import { EnvironmentVariablesFormValues } from '@/types';
import { Input } from 'components/shared/Input';
import { useToast } from 'components/shared/Toast';
import { ArrowRightCircle, Loader2 } from 'lucide-react';
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
import { EnvironmentVariablesFormValues } from 'types/types';
import {
VITE_LACONICD_CHAIN_ID,
VITE_WALLET_IFRAME_URL,
} from 'utils/constants';
} from '@/utils/constants';
import { FormControl, FormHelperText, MenuItem, Select } from '@mui/material';
import { ArrowRightCircle, Loader2 } from 'lucide-react';
import { useGQLClient } from '../../../context/GQLClientContext';
import { Button } from '../../shared/Button';
import { Heading } from '../../shared/Heading';
import ApproveTransactionModal from './ApproveTransactionModal';
import CheckBalanceIframe from './CheckBalanceIframe';
@ -39,6 +45,10 @@ type ConfigureFormValues = ConfigureDeploymentFormValues &
const DEFAULT_MAX_PRICE = '10000';
const TX_APPROVAL_TIMEOUT_MS = 60000;
/**
* Configure component that allows users to configure their deployment.
* @returns {JSX.Element} - The Configure component.
*/
const Configure = () => {
const [isLoading, setIsLoading] = useState(false);
const [deployers, setDeployers] = useState<Deployer[]>([]);
@ -106,6 +116,16 @@ const Configure = () => {
selectedNumProviders,
]);
/**
* Creates a project using the GQLClient.
* @async
* @function createProject
* @param {FieldValues} data - The form data.
* @param {AddEnvironmentVariableInput[]} envVariables - The environment variables.
* @param {string} senderAddress - The sender address.
* @param {string} txHash - The transaction hash.
* @returns {Promise<string>} - The project ID.
*/
const createProject = async (
data: FieldValues,
envVariables: AddEnvironmentVariableInput[],
@ -185,6 +205,15 @@ const Configure = () => {
}
};
/**
* Verifies a transaction using the GQLClient.
* @async
* @function verifyTx
* @param {string} senderAddress - The sender address.
* @param {string} txHash - The transaction hash.
* @param {string} amount - The amount.
* @returns {Promise<boolean>} - True if the transaction is valid, false otherwise.
*/
const verifyTx = async (
senderAddress: string,
txHash: string,
@ -199,6 +228,13 @@ const Configure = () => {
return isValid;
};
/**
* Handles the form submission.
* @async
* @function handleFormSubmit
* @param {FieldValues} createFormData - The form data.
* @returns {Promise<void>}
*/
const handleFormSubmit = useCallback(
async (createFormData: FieldValues) => {
try {
@ -315,11 +351,23 @@ const Configure = () => {
[client, createProject, dismiss, toast, amountToBePaid],
);
/**
* Fetches the deployers using the GQLClient.
* @async
* @function fetchDeployers
* @returns {Promise<void>}
*/
const fetchDeployers = useCallback(async () => {
const res = await client.getDeployers();
setDeployers(res.deployers);
}, [client]);
/**
* Handles the deployer change.
* @function onDeployerChange
* @param {string} selectedLrn - The selected LRN.
* @returns {void}
*/
const onDeployerChange = useCallback(
(selectedLrn: string) => {
const deployer = deployers.find((d) => d.deployerLrn === selectedLrn);
@ -328,6 +376,14 @@ const Configure = () => {
[deployers],
);
/**
* Handles the cosmos send tokens.
* @async
* @function cosmosSendTokensHandler
* @param {string} selectedAccount - The selected account.
* @param {string} amount - The amount.
* @returns {Promise<string>} - The transaction hash.
*/
const cosmosSendTokensHandler = useCallback(
async (selectedAccount: string, amount: string) => {
if (!selectedAccount) {
@ -399,6 +455,15 @@ const Configure = () => {
[client, dismiss, toast],
);
/**
* Requests a transaction.
* @async
* @function requestTx
* @param {string} sender - The sender.
* @param {string} recipient - The recipient.
* @param {string} amount - The amount.
* @returns {Promise<void>}
*/
const requestTx = async (
sender: string,
recipient: string,
@ -435,6 +500,11 @@ const Configure = () => {
}
}, [isBalanceSufficient]);
/**
* Handles the configure deployment.
* @function handleConfigureDeployment
* @returns {void}
*/
const handleConfigureDeployment = useCallback(() => {
methods.handleSubmit(handleFormSubmit)();
}, [handleFormSubmit, methods]);
@ -448,7 +518,7 @@ const Configure = () => {
</Heading>
<Heading
as="h5"
className="text-sm font-sans text-elements-low-em dark:text-foreground-secondaryu"
className="text-elements-low-em dark:text-foreground-secondaryu font-sans text-sm"
>
The app can be deployed by setting the deployer LRN for a single
deployment or by creating a deployer auction for multiple
@ -457,7 +527,7 @@ const Configure = () => {
</div>
</div>
<div className="flex flex-col gap-6 lg:gap-8 w-full">
<div className="lg:gap-8 flex flex-col w-full gap-6">
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(handleFormSubmit)}>
<div className="flex flex-col justify-start gap-4 mb-6">
@ -490,7 +560,7 @@ const Configure = () => {
<div className="flex flex-col justify-start gap-4 mb-6">
<Heading
as="h5"
className="text-sm font-sans text-elements-low-em dark:text-foreground-secondary"
className="text-elements-low-em dark:text-foreground-secondary font-sans text-sm"
>
The app will be deployed by the configured deployer
</Heading>
@ -500,14 +570,16 @@ const Configure = () => {
rules={{ required: true }}
render={({ field: { value, onChange }, fieldState }) => (
<FormControl fullWidth error={Boolean(fieldState.error)}>
<span className="text-sm dark:text-foreground text-elements-high-em dark:text-foreground mb-4">
<span className="dark:text-foreground text-elements-high-em mb-4 text-sm">
Select deployer LRN
</span>
<Select
value={value}
onChange={(event) => {
onChange(event.target.value);
onDeployerChange(event.target.value);
onChange((event.target as HTMLInputElement).value);
onDeployerChange(
(event.target as HTMLInputElement).value,
);
}}
displayEmpty
size="small"
@ -538,12 +610,12 @@ const Configure = () => {
<div className="flex flex-col justify-start gap-4 mb-6">
<Heading
as="h5"
className="text-sm font-sans text-elements-low-em dark:text-foreground-secondary"
className="text-elements-low-em dark:text-foreground-secondary font-sans text-sm"
>
Set the number of deployers and maximum price for each
deployment
</Heading>
<span className="text-sm text-elements-high-em dark:text-foreground">
<span className="text-elements-high-em dark:text-foreground text-sm">
Number of Deployers
</span>
<Controller
@ -553,15 +625,16 @@ const Configure = () => {
render={({ field: { value, onChange } }) => (
<Input
type="number"
value={value}
onChange={(e) => onChange(e)}
min={1}
value={value ?? ''}
onChange={(e) =>
onChange((e.target as HTMLInputElement).value)
}
/>
)}
/>
</div>
<div className="flex flex-col justify-start gap-4 mb-6">
<span className="text-sm text-elements-high-em dark:text-foreground">
<span className="text-elements-high-em dark:text-foreground text-sm">
Maximum Price (alnt)
</span>
<Controller
@ -569,41 +642,45 @@ const Configure = () => {
control={methods.control}
rules={{ required: true }}
render={({ field: { value, onChange } }) => (
<Input type="number" value={value} onChange={onChange} min={1} />
<Input
type="number"
value={value ?? ''}
onChange={(e) =>
onChange((e.target as HTMLInputElement).value)
}
// min={1}
/>
)}
/>
</div>
</>
)}
<Heading as="h4" className="md:text-lg font-medium mb-3">
<Heading as="h4" className="md:text-lg mb-3 font-medium">
Environment Variables
</Heading>
<div className="p-4 bg-slate-100 dark:bg-overlay3 rounded-lg mb-6">
<div className="bg-slate-100 dark:bg-overlay3 p-4 mb-6 rounded-lg">
<EnvironmentVariablesForm />
</div>
{selectedOption === 'LRN' && !selectedDeployer?.minimumPayment ? (
<div>
<Button
<IconButton
{...buttonSize}
type="submit"
title="Deploy"
disabled={isLoading || !selectedDeployer}
rightIcon={
isLoading ? (
<Loader2 className="ml-2 h-4 w-4 animate-spin" />
) : (
<ArrowRightCircle className="ml-2 h-4 w-4" />
)
}
>
{isLoading ? 'Deploying' : 'Deploy'}
</Button>
isLoading={isLoading}
loadingText="Deploying"
rightIcon={<ArrowRightCircle />}
/>
</div>
) : (
<div className="flex gap-4">
<Button
<IconButton
{...buttonSize}
type="submit"
title="Configure"
disabled={
isLoading ||
isPaymentLoading ||
@ -612,33 +689,16 @@ const Configure = () => {
amountToBePaid === '' ||
selectedNumProviders === ''
}
rightIcon={
isLoading || isPaymentLoading ? (
<Loader2 className="ml-2 h-4 w-4 animate-spin" />
) : (
<ArrowRightCircle className="ml-2 h-4 w-4" />
)
}
isLoading={isLoading || isPaymentLoading}
loadingText="Configuring"
rightIcon={<ArrowRightCircle />}
onClick={handleConfigureDeployment}
>
{isLoading ? (
<>
Configuring...
<Loader2 className="ml-2 h-4 w-4 animate-spin" />
</>
) : (
<>
Configure
<ArrowRightCircle className="ml-2 h-4 w-4" />
</>
)}
</Button>
/>
{isAccountsDataReceived && isBalanceSufficient !== undefined ? (
!selectedAccount || !isBalanceSufficient ? (
<div className="flex items-center gap-4">
<Button
{...buttonSize}
shape="default"
onClick={(e: any) => {
e.preventDefault();
setBalanceMessage('Waiting for payment');
@ -651,7 +711,7 @@ const Configure = () => {
>
Buy prepaid service
</Button>
<p className="text-gray-700 dark:text-gray-300">
<p className="dark:text-gray-300 text-gray-700">
{balanceMessage !== undefined ? (
<div className="flex items-center gap-2 text-white">
<Loader2 className="animate-spin w-5 h-5" />

View File

@ -2,14 +2,13 @@ import React from 'react';
import OauthPopup from 'react-oauth-popup';
import { useGQLClient } from '../../../context/GQLClientContext';
import { Button } from '../../shared/Button';
import { Github, GitBranch, MoreHorizontal } from 'lucide-react';
import { VITE_GITHUB_CLIENT_ID } from 'utils/constants';
import { IconButton } from '@/components/ui/extended/button-w-icons';
import { GitBranch, Github, MoreHorizontal } from 'lucide-react';
import { VITE_GITHUB_CLIENT_ID } from '../../../utils/constants';
import { Heading } from '../../shared/Heading';
import { IconWithFrame } from '../../shared/IconWithFrame';
import { useToast } from '../../shared/Toast';
import { MockConnectGitCard } from './MockConnectGitCard';
const SCOPES = 'public_repo user';
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`;
@ -18,6 +17,12 @@ interface ConnectAccountInterface {
onAuth: (token: string) => void;
}
/**
* ConnectAccount component that allows users to connect to their Git account.
* @param {Object} props - The component props.
* @param {function} props.onAuth - Callback function to be called when the user is authenticated.
* @returns {JSX.Element} - The ConnectAccount component.
*/
const ConnectAccount: React.FC<ConnectAccountInterface> = ({
onAuth: onToken,
}: ConnectAccountInterface) => {
@ -25,6 +30,13 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
const { toast, dismiss } = useToast();
/**
* Handles the code received from the OAuth popup.
* @async
* @function handleCode
* @param {string} code - The code received from the OAuth popup.
* @returns {Promise<void>}
*/
const handleCode = async (code: string) => {
// Pass code to backend and get access token
const {
@ -41,10 +53,10 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
// TODO: Use correct height
return (
<div className="dark:bg-overlay bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl">
<div className="dark:bg-overlay gap-7 rounded-2xl flex flex-col items-center justify-center h-full p-4 text-sm text-center bg-gray-100">
<div className="flex flex-col items-center max-w-[420px]">
{/** Icons */}
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex mb-7">
<div className="w-52 mb-7 inline-flex items-center justify-center h-16 gap-4">
<IconWithFrame icon={<GitBranch />} hasHighlight={false} />
<MoreHorizontal className="items-center gap-1.5 flex" />
<IconWithFrame
@ -55,16 +67,16 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
</div>
{/** Text */}
<div className="flex flex-col gap-1.5 mb-6">
<Heading className="text-xl font-medium dark:text-foreground">
<Heading className="dark:text-foreground text-xl font-medium">
Connect to your Git account
</Heading>
<p className="text-center text-elements-mid-em dark:text-foreground-secondary">
<p className="text-elements-mid-em dark:text-foreground-secondary text-center">
Once connected, you can import a repository from your account or
start with one of our templates.
</p>
</div>
{/** CTA Buttons */}
<div className="flex flex-col w-full sm:w-auto sm:flex-row gap-2 sm:gap-3">
<div className="sm:w-auto sm:flex-row sm:gap-3 flex flex-col w-full gap-2">
<OauthPopup
url={GITHUB_OAUTH_URL}
onCode={handleCode}
@ -73,27 +85,27 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
width={1000}
height={1000}
>
<Button
className="w-full sm:w-auto"
leftIcon={<Github />}
variant="primary"
<IconButton
className="sm:w-auto w-full"
leftIcon={<GitBranch />}
variant="default"
>
Connect to GitHub
</Button>
</IconButton>
</OauthPopup>
<Button
className="w-full sm:w-auto"
<IconButton
className="sm:w-auto w-full"
leftIcon={<Github />}
variant="primary"
variant="default"
>
Connect to GitTea
</Button>
</IconButton>
</div>
</div>
{/* TODO: Add ConnectAccountTabPanel */}
<MockConnectGitCard />
{/* <div className="rounded-l shadow p-2 flex-col justify-start items-start gap-2 inline-flex">
{/* <MockConnectGitCard /> */}
{/* <div className="inline-flex flex-col items-start justify-start gap-2 p-2 rounded-l shadow">
<ConnectAccountTabPanel />
</div> */}
</div>

View File

@ -1,7 +1,11 @@
import React from 'react';
import { Tabs } from 'components/shared/Tabs';
import { Tabs } from '@/components/shared/Tabs';
/**
* ConnectAccountTabPanel component that renders a tab panel for connecting accounts.
* @returns {JSX.Element} - The ConnectAccountTabPanel component.
*/
const ConnectAccountTabPanel: React.FC = () => {
return (
<Tabs

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 { Deployment } from 'gql-client';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { DeployStep, DeployStatus } from './DeployStep';
import { IconButton } from '@/components/ui';
import { useGQLClient } from '@/context/GQLClientContext';
import { AlertTriangle, Clock } from 'lucide-react';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
import { Heading } from '../../shared/Heading';
import { Button } from '../../shared/Button';
import { Clock, AlertTriangle } from 'lucide-react';
import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog';
import { useGQLClient } from 'context/GQLClientContext';
import { Heading } from '../../shared/Heading';
import { DeployStatus, DeployStep } from './DeployStep';
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
type RequestState =
@ -30,6 +29,10 @@ type Record = {
logAvailable: boolean;
};
/**
* Deploy component that displays the deployment status of a project.
* @returns {JSX.Element} - The Deploy component.
*/
const Deploy = () => {
const client = useGQLClient();
@ -49,6 +52,10 @@ const Deploy = () => {
navigate(`/${orgSlug}/projects/create`);
}, []);
/**
* Checks if the deployment has failed based on the record's last state.
* @returns {boolean} - True if the deployment has failed, false otherwise.
*/
const isDeploymentFailed = useMemo(() => {
if (!record) {
return false;
@ -62,6 +69,12 @@ const Deploy = () => {
}
}, [record]);
/**
* Fetches the deployment records from the deployer API.
* @async
* @function fetchDeploymentRecords
* @returns {Promise<void>}
*/
const fetchDeploymentRecords = useCallback(async () => {
if (!deployment) {
return;
@ -79,6 +92,12 @@ const Deploy = () => {
}
}, [deployment]);
/**
* Fetches the deployment information from the GQLClient.
* @async
* @function fetchDeployment
* @returns {Promise<void>}
*/
const fetchDeployment = useCallback(async () => {
if (!projectId) {
return;
@ -126,14 +145,14 @@ const Deploy = () => {
/>
</div>
</div>
<Button
<IconButton
onClick={handleOpen}
leftIcon={<AlertTriangle />}
aria-label="Cancel deployment"
size="sm"
variant="tertiary"
leftIcon={<AlertTriangle size={16} />}
>
Cancel
</Button>
</IconButton>
<CancelDeploymentDialog
handleCancel={handleOpen}
open={open}

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

View File

@ -1,12 +1,15 @@
import React, { useMemo } from 'react';
import { SegmentedControls } from 'components/shared/SegmentedControls';
import { useState } from 'react';
import { SegmentedControls } from '@/components/shared/SegmentedControls';
import React, { useMemo, useState } from 'react';
import { useMediaQuery } from 'usehooks-ts';
import { Github, GitBranch, LayoutDashboard } from 'lucide-react';
import { relativeTimeISO } from 'utils/time';
import templates from 'assets/templates';
import templates from '@/assets/templates';
import { relativeTimeISO } from '@/utils/time';
import { GitBranch, Github, LayoutDashboard } from 'lucide-react';
/**
* MockConnectGitCard component that allows users to import a repository or start with a template.
* @returns {JSX.Element} - The MockConnectGitCard component.
*/
export const MockConnectGitCard = () => {
const [segmentedControlsValue, setSegmentedControlsValue] =
useState<string>('import');
@ -43,15 +46,20 @@ export const MockConnectGitCard = () => {
},
];
/**
* Renders the content based on the selected segmented control value.
* If the value is 'import', it renders a list of mock project cards.
* If the value is 'template', it renders a grid of mock template cards.
*/
const renderContent = useMemo(() => {
if (segmentedControlsValue === 'import') {
return (
<div className="flex flex-col gap-2 relative z-0">
<div className="relative z-0 flex flex-col gap-2">
{IMPORT_CONTENT.map((repo, index) => (
<React.Fragment key={index}>
<MockProjectCard {...repo} />
{index !== IMPORT_CONTENT.length - 1 && (
<div className="border-b border-base-border" />
<div className="border-base-border border-b" />
)}
</React.Fragment>
))}
@ -59,7 +67,7 @@ export const MockConnectGitCard = () => {
);
}
return (
<div className="grid grid-cols-1 lg:grid-cols-2 relative z-0">
<div className="lg:grid-cols-2 relative z-0 grid grid-cols-1">
{templates.map((template, index) => (
<MockTemplateCard key={index} {...template} />
))}
@ -80,11 +88,19 @@ export const MockConnectGitCard = () => {
{renderContent}
{/* Shade */}
<div className="pointer-events-none z-99 absolute inset-0 rounded-2xl bg-gradient-to-t from-white dark:from-overlay to-transparent" />
<div className="z-99 rounded-2xl bg-gradient-to-t from-white dark:from-overlay to-transparent absolute inset-0 pointer-events-none" />
</div>
);
};
/**
* MockProjectCard component that displays a mock project card.
* @param {Object} props - The component props.
* @param {string} props.full_name - The full name of the repository.
* @param {string} [props.updated_at] - The last updated date of the repository.
* @param {string} [props.visibility] - The visibility of the repository.
* @returns {JSX.Element} - The MockProjectCard component.
*/
const MockProjectCard = ({
full_name,
updated_at,
@ -95,24 +111,24 @@ const MockProjectCard = ({
visibility?: string;
}) => {
return (
<div className="group flex items-start sm:items-center gap-3 pl-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized dark:hover:bg-background relative">
<div className="group sm:items-center rounded-xl hover:bg-base-bg-emphasized dark:hover:bg-background relative flex items-start gap-3 py-3 pl-3 cursor-pointer">
{/* Icon container */}
<div className="w-10 h-10 bg-base-bg dark:bg-background rounded-md justify-center items-center flex">
<Github className="h-5 w-5" />
<div className="bg-base-bg dark:bg-background flex items-center justify-center w-10 h-10 rounded-md">
<Github className="w-5 h-5" />
</div>
{/* Content */}
<div className="flex flex-1 gap-3 flex-wrap">
<div className="flex flex-wrap flex-1 gap-3">
<div className="flex flex-col items-start gap-1">
<p className="text-elements-high-em text-sm dark:text-foreground font-medium tracking-[-0.006em]">
{full_name}
</p>
<p className="text-elements-low-em text-xs dark:text-foreground-secondary">
<p className="text-elements-low-em dark:text-foreground-secondary text-xs">
{updated_at && relativeTimeISO(updated_at)}
</p>
</div>
{visibility === 'private' && (
<div className="bg-orange-50 border border-orange-200 px-2 py-1 flex items-center gap-1 rounded-lg text-xs text-orange-600 h-fit">
<GitBranch className="h-4 w-4" />
<div className="bg-orange-50 h-fit flex items-center gap-1 px-2 py-1 text-xs text-orange-600 border border-orange-200 rounded-lg">
<GitBranch className="w-4 h-4" />
Private
</div>
)}
@ -121,11 +137,17 @@ const MockProjectCard = ({
);
};
/**
* MockTemplateCard component that displays a mock template card.
* @param {Object} props - The component props.
* @param {string} props.name - The name of the template.
* @returns {JSX.Element} - The MockTemplateCard component.
*/
const MockTemplateCard = ({ name }: { name: string }) => {
return (
<div className="flex items-center gap-2">
<LayoutDashboard className="h-4 w-4" />
<p className="text-sm font-medium text-elements-primary">{name}</p>
<LayoutDashboard className="w-4 h-4" />
<p className="text-elements-primary text-sm font-medium">{name}</p>
</div>
);
};

View File

@ -3,17 +3,33 @@ import { useNavigate, useParams } from 'react-router-dom';
import { Spinner } from '@snowballtools/material-tailwind-react-fork';
import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast';
import { useGQLClient } from 'context/GQLClientContext';
import { useToast } from '@/components/shared/Toast';
import { Button } from '@/components/ui';
import { useGQLClient } from '@/context/GQLClientContext';
import { relativeTimeISO } from '@/utils/time';
import { ArrowRight, Github, Lock } from 'lucide-react';
import { relativeTimeISO } from 'utils/time';
import { GitRepositoryDetails } from '../../../../types/types';
/**
* Props for the ProjectRepoCard component.
*/
interface ProjectRepoCardProps {
/**
* The repository to display in the card.
*/
repository: GitRepositoryDetails;
}
/**
* ProjectRepoCard Component
*
* This component renders a card displaying information about a GitHub repository, including its name,
* update date, and visibility. It allows users to select the repository to create a new project.
* It uses `useNavigate` to navigate to the configuration page and `useToast` to display error messages.
*
* @param {ProjectRepoCardProps} props - The props for the ProjectRepoCard component.
* @returns {JSX.Element} - The ProjectRepoCard component.
*/
export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
repository,
}) => {
@ -41,15 +57,15 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
return (
<div
className="group flex items-start sm:items-center gap-3 px-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized relative"
className="group sm:items-center rounded-xl hover:bg-base-bg-emphasized relative flex items-start gap-3 px-3 py-3 cursor-pointer"
onClick={createProject}
>
{/* Icon container */}
<div className="w-10 h-10 bg-base-bg rounded-md justify-center items-center flex dark:bg-overlay">
<div className="bg-base-bg dark:bg-overlay flex items-center justify-center w-10 h-10 rounded-md">
<Github />
</div>
{/* Content */}
<div className="flex flex-1 gap-3 flex-wrap">
<div className="flex flex-wrap flex-1 gap-3">
<div className="flex flex-col gap-1">
<p className="text-elements-high-em dark:text-foreground text-sm font-medium tracking-[-0.006em]">
{repository.full_name}
@ -59,7 +75,7 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
</p>
</div>
{repository.visibility === 'private' && (
<div className="bg-orange-50 border border-orange-200 px-2 py-1 flex items-center gap-1 rounded-lg text-xs text-orange-600 dark:text-error h-fit">
<div className="bg-orange-50 dark:text-error h-fit flex items-center gap-1 px-2 py-1 text-xs text-orange-600 border border-orange-200 rounded-lg">
<Lock />
Private
</div>
@ -67,13 +83,12 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
</div>
{/* Right action */}
{isLoading ? (
<Spinner className="h-4 w-4 absolute right-3" />
<Spinner className="right-3 absolute w-4 h-4" />
) : (
<Button
variant="tertiary"
variant="secondary"
size="sm"
iconOnly
className="sm:group-hover:flex hidden absolute right-3"
className="sm:group-hover:flex right-3 absolute hidden"
>
<ArrowRight />
</Button>

View File

@ -1,22 +1,28 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import assert from 'assert';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDebounce } from 'usehooks-ts';
import { ProjectRepoCard } from 'components/projects/create/ProjectRepoCard';
import { ProjectRepoCard } from '@/components/projects/create/ProjectRepoCard';
import { Select, SelectOption } from '@/components/shared/Select';
import { IconButton } from '@/components/ui/extended/button-w-icons';
import { IconInput } from '@/components/ui/extended/input-w-icons';
import { useOctokit } from '@/context/OctokitContext';
import { Github, RotateCw, Search } from 'lucide-react';
import { GitOrgDetails, GitRepositoryDetails } from '../../../../types/types';
import {
Github,
RotateCw,
Search,
} from 'lucide-react';
import { Select, SelectOption } from 'components/shared/Select';
import { Input } from 'components/shared/Input';
import { Button } from 'components/shared/Button';
import { useOctokit } from 'context/OctokitContext';
const DEFAULT_SEARCHED_REPO = '';
const REPOS_PER_PAGE = 5;
/**
* RepositoryList Component
*
* This component renders a list of repositories fetched from GitHub based on the selected account
* and search criteria. It allows users to select an organization or their personal account and search
* for repositories within that account.
* It uses `useOctokit` to fetch repositories and `ProjectRepoCard` to display each repository.
*
* @returns {JSX.Element} - The RepositoryList component.
*/
export const RepositoryList = () => {
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
const [selectedAccount, setSelectedAccount] = useState<SelectOption>();
@ -128,7 +134,7 @@ export const RepositoryList = () => {
return (
<section className="space-y-3">
{/* Dropdown and search */}
<div className="flex flex-col lg:flex-row gap-0 lg:gap-3 items-center">
<div className="lg:flex-row lg:gap-3 flex flex-col items-center gap-0">
<div className="lg:basis-1/3 w-full">
<Select
options={options}
@ -139,8 +145,8 @@ export const RepositoryList = () => {
onChange={(value) => setSelectedAccount(value as SelectOption)}
/>
</div>
<div className="basis-2/3 flex w-full flex-grow">
<Input
<div className="basis-2/3 flex flex-grow w-full">
<IconInput
className="w-full"
value={searchedRepo}
placeholder="Search for repository"
@ -164,18 +170,18 @@ export const RepositoryList = () => {
))}
</div>
) : (
<div className="mt-4 p-6 flex flex-col gap-4 items-center justify-center">
<div className="flex flex-col items-center justify-center gap-4 p-6 mt-4">
<p className="text-elements-high-em dark:text-foreground font-sans">
No repository found
</p>
<Button
variant="tertiary"
<IconButton
variant="outline"
leftIcon={<RotateCw />}
size="sm"
onClick={handleResetFilters}
>
Reset filters
</Button>
</IconButton>
</div>
)}
</section>

View File

@ -1,10 +1,10 @@
import { Button } from 'components/shared/Button';
import { Tag } from 'components/shared/Tag';
import { useToast } from 'components/shared/Toast';
import { Tag } from '@/components/shared/Tag';
import { useToast } from '@/components/shared/Toast';
import { Button } from '@/components/ui';
import { cn } from '@/utils/classnames';
import { GitBranch } from 'lucide-react';
import React, { ComponentPropsWithoutRef, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { cn } from 'utils/classnames';
export interface TemplateDetail {
id: string;
@ -60,11 +60,11 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
onClick={handleClick}
>
{/* Icon */}
<div className="px-1 py-1 rounded-xl bg-base-bg border border-border-interactive/10 shadow-card-sm">
<div className="rounded-xl bg-base-bg border-border-interactive/10 shadow-card-sm px-1 py-1 border">
<GitBranch />
</div>
{/* Name */}
<p className="flex-1 text-left text-sm tracking-tighter text-elements-high-em">
<p className="text-elements-high-em flex-1 text-sm tracking-tighter text-left">
{template.name}
</p>
{template?.isComingSoon ? (
@ -73,10 +73,10 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
</Tag>
) : (
<Button
variant="tertiary"
variant="secondary"
size="sm"
iconOnly
className="group-hover:flex hidden absolute right-3"
// iconOnly
className="group-hover:flex right-3 absolute hidden"
>
<GitBranch />
</Button>

View File

@ -10,42 +10,76 @@ import { useCallback, useState } from 'react';
import {
Avatar,
AvatarFallback,
Modal as Dialog,
ModalContent as DialogContent,
ModalFooter as DialogFooter,
ModalTitle as DialogTitle,
Button,
Dialog,
DialogContent,
DialogFooter,
DialogTitle,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from 'components/shared';
} from '@/components/ui';
import { Button } from 'components/shared/Button';
import { GitBranch, Clock, CheckCircle, AlertTriangle } from 'lucide-react';
import { Heading } from 'components/shared/Heading';
import { OverflownText } from 'components/shared/OverflownText';
import { Tag } from 'components/shared/Tag';
import { getInitials } from 'utils/geInitials';
import { relativeTimeMs } from 'utils/time';
import { Heading } from '@/components/shared/Heading';
import { OverflownText } from '@/components/shared/OverflownText';
import { Tag } from '@/components/shared/Tag';
import { getInitials } from '@/utils/geInitials';
import { relativeTimeMs } from '@/utils/time';
import { AlertTriangle, CheckCircle, Clock, GitBranch } from 'lucide-react';
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
import { formatAddress } from '../../../../utils/format';
import { DeploymentMenu } from './DeploymentMenu';
interface DeployDetailsCardProps {
deployment: Deployment;
currentDeployment: Deployment;
onUpdate: () => Promise<void>;
project: Project;
prodBranchDomains: Domain[];
}
const STATUS_COLORS: Record<DeploymentStatus, 'attention' | 'positive' | 'negative' | 'neutral'> = {
/**
* Type definition for the status colors.
*/
const STATUS_COLORS: Record<
DeploymentStatus,
'attention' | 'positive' | 'negative' | 'neutral'
> = {
[DeploymentStatus.Building]: 'attention',
[DeploymentStatus.Ready]: 'positive',
[DeploymentStatus.Error]: 'negative',
[DeploymentStatus.Deleting]: 'neutral',
};
/**
* Props for the DeploymentDetailsCard component.
*/
interface DeployDetailsCardProps {
/**
* The deployment to display details for.
*/
deployment: Deployment;
/**
* The current deployment.
*/
currentDeployment: Deployment;
/**
* Callback function to update deployments.
*/
onUpdate: () => Promise<void>;
/**
* The project the deployment belongs to.
*/
project: Project;
/**
* The domains for the production branch of the project.
*/
prodBranchDomains: Domain[];
}
/**
* DeploymentDetailsCard Component
*
* This component renders a card displaying detailed information about a deployment, including its URL, status,
* commit details, and deployment information. It also provides a menu for managing the deployment.
* It uses `DeploymentMenu` for deployment management actions and `Tooltip` for displaying build logs.
*
* @param {DeployDetailsCardProps} props - The props for the DeploymentDetailsCard component.
* @returns {JSX.Element} - The DeploymentDetailsCard component.
*/
const DeploymentDetailsCard = ({
deployment,
currentDeployment,
@ -102,7 +136,11 @@ const DeploymentDetailsCard = ({
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className={className} style={{ cursor: 'pointer' }} onClick={fetchDeploymentLogs}>
<div
className={className}
style={{ cursor: 'pointer' }}
onClick={fetchDeploymentLogs}
>
<Tag
type={STATUS_COLORS[deployment.status]}
size="xs"
@ -123,13 +161,13 @@ const DeploymentDetailsCard = ({
);
return (
<div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative">
<div className="md:flex-row last:mb-0 last:pb-4 border-border-separator last:border-b-transparent relative flex flex-col gap-6 px-3 py-4 pb-6 mb-2 border-b">
<div className="flex-1 flex justify-between w-full md:max-w-[30%] lg:max-w-[33%]">
<div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full">
{/* DEPLOYMENT URL */}
{deployment.url && (
<Heading
className="text-sm font-medium text-elements-high-em tracking-tight"
className="text-elements-high-em text-sm font-medium tracking-tight"
as="h2"
>
<OverflownText content={deployment.url}>
@ -138,32 +176,30 @@ const DeploymentDetailsCard = ({
</Heading>
)}
{deployment.deployer.deployerLrn && (
<span className="text-sm text-elements-low-em tracking-tight block mt-2">
<span className="text-elements-low-em block mt-2 text-sm tracking-tight">
Deployer LRN: {deployment.deployer.deployerLrn}
</span>
)}
<span className="text-sm text-elements-low-em tracking-tight block">
<span className="text-elements-low-em block text-sm tracking-tight">
{deployment.environment === Environment.Production
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
: 'Preview'}
</span>
</div>
</div>
{/* DEPLOYMENT STATUS */}
{renderDeploymentStatus('w-[10%] max-w-[110px] hidden md:flex h-fit')}
{/* DEPLOYMENT COMMIT DETAILS */}
<div className="flex w-full justify-between md:w-[25%]">
<div className="text-sm max-w-[60%] md:max-w-full space-y-2 w-full text-elements-low-em">
<span className="flex gap-1.5 items-center">
<GitBranch className="h-4 w-4" />
<GitBranch className="w-4 h-4" />
<OverflownText content={deployment.branch}>
{deployment.branch}
</OverflownText>
</span>
<span className="flex w-full gap-2 items-center">
<div className="h-4 w-4" />
<span className="flex items-center w-full gap-2">
<div className="w-4 h-4" />
<OverflownText content={deployment.commitMessage}>
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
{deployment.commitMessage}
@ -172,29 +208,32 @@ const DeploymentDetailsCard = ({
</div>
{renderDeploymentStatus('flex md:hidden h-fit')}
</div>
{/* DEPLOYMENT INFOs */}
<div className="md:ml-auto w-full md:max-w-[312px] md:w-[30%] gap-1 2xl:gap-5 flex items-center justify-between text-elements-low-em text-sm">
<div className="flex md:w-[70%] xl:items-center gap-2 flex-1 xl:flex-row md:flex-col">
<div className="flex gap-2 items-center">
<Clock className="h-4 w-4" />
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
<OverflownText content={relativeTimeMs(deployment.createdAt) ?? ''}>
{relativeTimeMs(deployment.createdAt)}
</OverflownText>
</div>
<div className="flex gap-2 items-center">
<div className="flex items-center gap-2">
<div>
<Avatar className="lg:size-5 2xl:size-6">
<AvatarFallback>{getInitials(deployment.createdBy.name ?? '')}</AvatarFallback>
<AvatarFallback>
{getInitials(deployment.createdBy.name ?? '')}
</AvatarFallback>
</Avatar>
</div>
<OverflownText content={formatAddress(deployment.createdBy?.name ?? '')}>
<OverflownText
content={formatAddress(deployment.createdBy?.name ?? '')}
>
{formatAddress(deployment.createdBy.name ?? '')}
</OverflownText>
</div>
</div>
<DeploymentMenu
className="ml-auto md:static absolute top-4 right-0"
className="md:static top-4 absolute right-0 ml-auto"
deployment={deployment}
currentDeployment={currentDeployment}
onUpdate={onUpdate}
@ -202,6 +241,7 @@ const DeploymentDetailsCard = ({
prodBranchDomains={prodBranchDomains}
/>
</div>
{/* @ts-ignore */}
<Dialog open={openDialog} onClose={handleCloseDialog}>
<DialogTitle>Deployment logs</DialogTitle>
<DialogContent className="bg-[rgba(0,0,0,0.9)] p-8 rounded-lg mx-2 text-gray-400 text-sm">

View File

@ -1,16 +1,12 @@
import { Deployment } from 'gql-client';
import { relativeTimeMs } from 'utils/time';
import { Avatar } from '@/components/shared/Avatar';
import { OverflownText } from '@/components/shared/OverflownText';
import { Tag, TagProps } from '@/components/shared/Tag';
import { getInitials } from '@/utils/geInitials';
import { relativeTimeMs } from '@/utils/time';
import { Clock, GitBranch, GitCommit } from 'lucide-react';
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
import {
GitBranch,
Clock,
GitCommit,
} from 'lucide-react';
import { Avatar } from 'components/shared/Avatar';
import { getInitials } from 'utils/geInitials';
import { OverflownText } from 'components/shared/OverflownText';
import { Tag, TagProps } from 'components/shared/Tag';
interface DeploymentDialogBodyCardProps {
deployment: Deployment;

View File

@ -7,11 +7,12 @@ import {
MenuList,
} from '@snowballtools/material-tailwind-react-fork';
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
import { DeleteDeploymentDialog } from 'components/projects/Dialog/DeleteDeploymentDialog';
import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast';
import { useGQLClient } from 'context/GQLClientContext';
import { ChangeStateToProductionDialog } from '@/components/projects/Dialog/ChangeStateToProductionDialog';
import { DeleteDeploymentDialog } from '@/components/projects/Dialog/DeleteDeploymentDialog';
import { useToast } from '@/components/shared/Toast';
import { Button } from '@/components/ui';
import { useGQLClient } from '@/context/GQLClientContext';
import { cn } from '@/utils/classnames';
import { Deployment, Domain, Environment, Project } from 'gql-client';
import {
Link,
@ -19,18 +20,46 @@ import {
Rocket,
RotateCw,
Trash2,
Undo
Undo,
} from 'lucide-react';
import { cn } from 'utils/classnames';
/**
* Props for the DeploymentMenu component.
*/
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
/**
* The deployment to display the menu for.
*/
deployment: Deployment;
/**
* The current deployment.
*/
currentDeployment: Deployment;
/**
* Callback function to update deployments.
*/
onUpdate: () => Promise<void>;
/**
* The project the deployment belongs to.
*/
project: Project;
/**
* The domains for the production branch of the project.
*/
prodBranchDomains: Domain[];
}
/**
* DeploymentMenu Component
*
* This component renders a menu for managing deployments, including options to visit the deployment URL,
* change the deployment to production, redeploy, rollback, and delete the deployment.
* It uses `useGQLClient` to perform actions such as updating, redeploying, rolling back, and deleting deployments.
* It also uses `ChangeStateToProductionDialog` and `DeleteDeploymentDialog` for confirmation of actions.
*
* @param {DeploymentMenuProps} props - The props for the DeploymentMenu component.
* @returns {JSX.Element} - The DeploymentMenu component.
*/
export const DeploymentMenu = ({
deployment,
currentDeployment,
@ -150,9 +179,7 @@ export const DeploymentMenu = ({
<Menu placement="bottom-start">
<MenuHandler>
<Button
shape="default"
size="xs"
variant="unstyled"
variant="ghost"
className={cn(
'h-8 w-8 rounded-full border border-transparent transition-colors background-transparent',
'[&[aria-expanded=true]]:border [&[aria-expanded=true]]:border-border-interactive [&[aria-expanded=true]]:bg-controls-tertiary [&[aria-expanded=true]]:shadow-button',

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import { Link } from 'react-router-dom';
import { GitCommitWithBranch } from '../../../../../types/types';
import { Avatar } from 'components/shared/Avatar';
import { Button } from 'components/shared/Button';
import { Check, GitBranch } from 'lucide-react';
import { Avatar } from '@/components/shared/Avatar';
import { IconButton } from '@/components/ui';
import { GitCommitWithBranch } from '@/types';
import { getInitials } from '@/utils/geInitials';
import { formatDistance } from 'date-fns';
import { getInitials } from 'utils/geInitials';
import { Check, GitBranch } from 'lucide-react';
import { Link } from 'react-router-dom';
interface ActivityCardProps {
activity: GitCommitWithBranch;
@ -24,7 +24,7 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
<Link
to={activity.html_url}
target="_blank"
className="p-3 gap-2 focus-within:ring-2 focus-within:ring-controls-primary/40 focus:outline-none rounded-xl transition-colors hover:bg-base-bg-alternate flex group"
className="focus-within:ring-2 focus-within:ring-controls-primary/40 focus:outline-none rounded-xl hover:bg-base-bg-alternate group flex gap-2 p-3 transition-colors"
>
<div>
<Avatar
@ -38,7 +38,7 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
<span className="text-elements-high-em text-sm font-medium tracking-tight">
{activity.commit.author?.name}
</span>
<span className="text-elements-low-em text-xs flex items-center gap-2">
<span className="text-elements-low-em flex items-center gap-2 text-xs">
<span title={formattedDate} className="whitespace-nowrap">
{formattedDate}
</span>
@ -51,7 +51,7 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
<span
title={activity.branch.name}
// pseudo to increase hover area
className="before:absolute relative before:h-5 before:-top-4 before:inset-x-0"
className="before:absolute before:h-5 before:-top-4 before:inset-x-0 relative"
>
<span className="line-clamp-1">{activity.branch.name}</span>
</span>
@ -59,17 +59,16 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
</span>
<p
title={activity.commit.message}
className="text-sm line-clamp-4 tracking-tight text-elements-mid-em mt-2"
className="line-clamp-4 text-elements-mid-em mt-2 text-sm tracking-tight"
>
{activity.commit.message}
</p>
</div>
<Button
<IconButton
aria-label="Go to commit"
variant="unstyled"
tabIndex={-1}
className="p-0 text-elements-low-em group-focus-within:opacity-100 group-hover:opacity-100 opacity-0 transition-all"
leftIcon={<Check className="w-5 h-5" />}
className="text-elements-low-em group-focus-within:opacity-100 group-hover:opacity-100 p-0 transition-all opacity-0"
leftIcon={<Check />}
/>
</Link>
{/* Separator calc => 100% - 36px (avatar) - 12px (padding-left) - 8px (gap)

View File

@ -8,9 +8,11 @@ import {
DialogTitle,
} from '@mui/material';
import { Button, Heading, Tag } from 'components/shared';
import { Heading } from '@/components/shared/Heading';
import { Tag } from '@/components/shared/Tag';
import { Button } from '@/components/ui';
import { useGQLClient } from '@/context/GQLClientContext';
import { CheckCircle, Loader2 } from 'lucide-react';
import { useGQLClient } from 'context/GQLClientContext';
const WAIT_DURATION = 5000;
@ -82,9 +84,9 @@ export const AuctionCard = ({ project }: { project: Project }) => {
return (
<>
<div className="p-3 gap-2 rounded-xl border dark:border-overlay3 border-gray-200 transition-colors hover:bg-base-bg-alternate dark:hover:bg-overlay3 flex flex-col mt-8">
<div className="flex justify-between items-center">
<Heading className="text-lg leading-6 font-medium">
<div className="rounded-xl dark:border-overlay3 hover:bg-base-bg-alternate dark:hover:bg-overlay3 flex flex-col gap-2 p-3 mt-8 transition-colors border border-gray-200">
<div className="flex items-center justify-between">
<Heading className="text-lg font-medium leading-6">
Auction details
</Heading>
<Button onClick={handleOpenDialog} variant="secondary" size="sm">
@ -92,7 +94,7 @@ export const AuctionCard = ({ project }: { project: Project }) => {
</Button>
</div>
<div className="flex justify-between items-center mt-2">
<div className="flex items-center justify-between mt-2">
<span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Auction Id
</span>
@ -101,7 +103,7 @@ export const AuctionCard = ({ project }: { project: Project }) => {
</span>
</div>
<div className="flex justify-between items-center mt-1">
<div className="flex items-center justify-between mt-1">
<span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Auction Status
</span>
@ -124,7 +126,7 @@ export const AuctionCard = ({ project }: { project: Project }) => {
</p>
))}
<div className="flex justify-between items-center mt-1">
<div className="flex items-center justify-between mt-1">
<span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Deployer Funds Status
</span>
@ -169,9 +171,7 @@ export const AuctionCard = ({ project }: { project: Project }) => {
)}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>
Close
</Button>
<Button onClick={handleCloseDialog}>Close</Button>
</DialogActions>
</Dialog>
</>

View File

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

View File

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

View File

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

View File

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

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