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