Added project pages and cards, most of the screens in chris samuels figma design document. Still need to implement project initialization modal and walkthrough, connect to backend and connect to wallet (maybe be beyond scope of this project)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
|
||||
CLERK_SECRET_KEY=
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL= # wherever your wallet is running
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID= # the appropriate chain ID for your network
|
||||
NEXT_PUBLIC_API_URL=
|
||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=
|
||||
@@ -0,0 +1,12 @@
|
||||
node_modules
|
||||
.next/
|
||||
.turbo/
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
|
||||
# clerk configuration (can include secrets)
|
||||
/.clerk/
|
||||
.vercel
|
||||
.clerk/
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
// Project-specific formatter choice
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.formatOnSave": true,
|
||||
|
||||
// TypeScript configuration
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
|
||||
// Code actions
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.addMissingImports.ts": "explicit",
|
||||
"source.organizeImports.biome": "explicit",
|
||||
"source.removeUnused.ts": "explicit"
|
||||
},
|
||||
|
||||
// Language-specific formatters for this project
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
|
||||
// TypeScript-specific performance settings for this project
|
||||
"typescript.preferGoToSourceDefinition": true,
|
||||
"typescript.suggest.paths": true,
|
||||
"typescript.tsserver.disableAutomaticTypeAcquisition": false,
|
||||
|
||||
// TypeScript server project-specific settings
|
||||
"typescript.tsserver.maxTsServerMemory": 8192,
|
||||
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
|
||||
"typescript.tsserver.enableTracing": false,
|
||||
|
||||
// For large TypeScript projects
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/dist": true,
|
||||
"**/build": true
|
||||
},
|
||||
|
||||
// For better JSDoc documentation
|
||||
"javascript.suggest.completeJSDocs": true
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["../../biome.json"]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "../../services/ui/tailwind.config.ts",
|
||||
"css": "../../services/ui/src/styles/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"hooks": "@/hooks",
|
||||
"lib": "@/lib",
|
||||
"utils": "@workspace/ui/lib/utils",
|
||||
"ui": "@workspace/ui/components"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
// Load environment variables from .env.development.local
|
||||
dotenv.config({ path: '.env.development.local' })
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ['@workspace/ui'],
|
||||
env: {
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
|
||||
process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
||||
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY
|
||||
}
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"name": "deploy-fe",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "NODE_OPTIONS='--inspect' next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "biome check .",
|
||||
"lint:fix": "biome check --write .",
|
||||
"format": "biome format .",
|
||||
"format:fix": "biome format --write .",
|
||||
"check-types": "tsc --noEmit",
|
||||
"fix-types": "tsc --noEmit --pretty --incremental"
|
||||
},
|
||||
"dependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@clerk/nextjs": "^6.12.4",
|
||||
"@clerk/themes": "^2.2.20",
|
||||
"@hookform/resolvers": "^4.1.2",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@octokit/webhooks-types": "^7.6.1",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.2",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.6",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.6",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-menubar": "^1.1.6",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toggle": "^1.1.2",
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@radix-ui/react-visually-hidden": "^1.1.2",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"axios": "^1.8.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"downshift": "^9.0.9",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "0.477.0",
|
||||
"next": "^15.2.1",
|
||||
"next-themes": "^0.4.4",
|
||||
"octokit": "^3.1.2",
|
||||
"react": "^19.0.0",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "^2.15.1",
|
||||
"siwe": "^3.0.0",
|
||||
"sonner": "^2.0.1",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "18.3.0",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@workspace/gql-client": "workspace:*",
|
||||
"@workspace/typescript-config": "workspace:*",
|
||||
"dotenv": "^16.4.7",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from '@workspace/ui/postcss.config'
|
||||
@@ -0,0 +1,220 @@
|
||||
./.env.local
|
||||
./.gitignore
|
||||
./.turbo/turbo-build.log
|
||||
./.vscode/settings.json
|
||||
./biome.jsonc
|
||||
./components.json
|
||||
./next-env.d.ts
|
||||
./next.config.mjs
|
||||
./package.json
|
||||
./postcss.config.mjs
|
||||
./repo_structure.txt
|
||||
./src/actions/github.ts
|
||||
./src/app/(web3-authenticated)/(dashboard)/documentation/DocumentationPlaceholder.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/documentation/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/home/loading.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/home/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/layout.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/(create)/cr/(configure)/cf/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/(create)/cr/(deploy)/dp/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/(create)/cr/(success)/sc/[id]/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/(create)/cr/(template)/tm/(configure)/cf/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/(create)/cr/(template)/tm/(deploy)/dp/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(deployments)/dep/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(integrations)/int/GitPage.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(integrations)/int/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/(collaborators)/col/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/(domains)/dom/(add)/cf/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/(domains)/dom/(add)/config/cf/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/(environment-variables)/env/EnvVarsPage.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/(environment-variables)/env/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/(git)/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/ProjectSettingsPage.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/deployments/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/layout.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/loading.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/error.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/loading.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/projects/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/purchase/BuyServices.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/purchase/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/store/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/support/SupportPlaceholder.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/support/page.tsx
|
||||
./src/app/(web3-authenticated)/(dashboard)/wallet/page.tsx
|
||||
./src/app/(web3-authenticated)/layout.tsx
|
||||
./src/app/actions/github.ts
|
||||
./src/app/api/auth/route.ts
|
||||
./src/app/api/github/webhook/route.ts
|
||||
./src/app/favicon.ico
|
||||
./src/app/layout.tsx
|
||||
./src/app/loading.tsx
|
||||
./src/app/page.tsx
|
||||
./src/app/sign-in/[[...sign-in]]/page.tsx
|
||||
./src/components/assets/laconic-mark.tsx
|
||||
./src/components/core/dropdown/Dropdown.tsx
|
||||
./src/components/core/dropdown/README.md
|
||||
./src/components/core/dropdown/index.ts
|
||||
./src/components/core/dropdown/types.ts
|
||||
./src/components/core/format-milli-second/FormatMilliSecond.tsx
|
||||
./src/components/core/format-milli-second/README.md
|
||||
./src/components/core/format-milli-second/index.ts
|
||||
./src/components/core/format-milli-second/types.ts
|
||||
./src/components/core/logo/Logo.tsx
|
||||
./src/components/core/logo/README.md
|
||||
./src/components/core/logo/index.ts
|
||||
./src/components/core/logo/types.ts
|
||||
./src/components/core/search-bar/README.md
|
||||
./src/components/core/search-bar/SearchBar.tsx
|
||||
./src/components/core/search-bar/index.ts
|
||||
./src/components/core/search-bar/types.ts
|
||||
./src/components/core/stepper/README.md
|
||||
./src/components/core/stepper/Stepper.tsx
|
||||
./src/components/core/stepper/index.ts
|
||||
./src/components/core/stepper/types.ts
|
||||
./src/components/core/stop-watch/README.md
|
||||
./src/components/core/stop-watch/StopWatch.tsx
|
||||
./src/components/core/stop-watch/index.ts
|
||||
./src/components/core/stop-watch/types.ts
|
||||
./src/components/core/vertical-stepper/README.md
|
||||
./src/components/core/vertical-stepper/VerticalStepper.tsx
|
||||
./src/components/core/vertical-stepper/index.ts
|
||||
./src/components/core/vertical-stepper/types.ts
|
||||
./src/components/foundation/coming-soon-overlay/ComingSoonOverlay.tsx
|
||||
./src/components/foundation/coming-soon-overlay/index.ts
|
||||
./src/components/foundation/github-session-button/GitHubSessionButton.tsx
|
||||
./src/components/foundation/github-session-button/README.md
|
||||
./src/components/foundation/github-session-button/index.ts
|
||||
./src/components/foundation/github-session-button/types.ts
|
||||
./src/components/foundation/index.ts
|
||||
./src/components/foundation/laconic-icon/LaconicIcon.tsx
|
||||
./src/components/foundation/laconic-icon/README.md
|
||||
./src/components/foundation/laconic-icon/index.ts
|
||||
./src/components/foundation/laconic-icon/types.ts
|
||||
./src/components/foundation/loading/loading-overlay/LoadingOverlay.tsx
|
||||
./src/components/foundation/loading/loading-overlay/README.md
|
||||
./src/components/foundation/loading/loading-overlay/index.ts
|
||||
./src/components/foundation/navigation-wrapper/NavigationWrapper.tsx
|
||||
./src/components/foundation/navigation-wrapper/README.md
|
||||
./src/components/foundation/navigation-wrapper/index.ts
|
||||
./src/components/foundation/page-header/PageHeader.tsx
|
||||
./src/components/foundation/page-header/README.md
|
||||
./src/components/foundation/page-header/index.ts
|
||||
./src/components/foundation/page-wrapper/PageWrapper.tsx
|
||||
./src/components/foundation/page-wrapper/README.md
|
||||
./src/components/foundation/page-wrapper/index.ts
|
||||
./src/components/foundation/project-search-bar/ProjectSearchBar.tsx
|
||||
./src/components/foundation/project-search-bar/README.md
|
||||
./src/components/foundation/project-search-bar/index.ts
|
||||
./src/components/foundation/project-search-bar/types.ts
|
||||
./src/components/foundation/top-navigation/README.md
|
||||
./src/components/foundation/top-navigation/dark-mode-toggle/DarkModeToggle.tsx
|
||||
./src/components/foundation/top-navigation/dark-mode-toggle/README.md
|
||||
./src/components/foundation/top-navigation/dark-mode-toggle/index.ts
|
||||
./src/components/foundation/top-navigation/index.ts
|
||||
./src/components/foundation/top-navigation/main-navigation/MainNavigation.tsx
|
||||
./src/components/foundation/top-navigation/main-navigation/README.md
|
||||
./src/components/foundation/top-navigation/main-navigation/index.ts
|
||||
./src/components/foundation/top-navigation/navigation-item/NavigationItem.tsx
|
||||
./src/components/foundation/top-navigation/navigation-item/README.md
|
||||
./src/components/foundation/top-navigation/navigation-item/index.ts
|
||||
./src/components/foundation/top-navigation/types.ts
|
||||
./src/components/foundation/top-navigation/wallet-session-badge/README.md
|
||||
./src/components/foundation/top-navigation/wallet-session-badge/WalletSessionBadge.tsx
|
||||
./src/components/foundation/top-navigation/wallet-session-badge/index.ts
|
||||
./src/components/foundation/types.ts
|
||||
./src/components/foundation/wallet-session-id/README.md
|
||||
./src/components/foundation/wallet-session-id/WalletSessionId.tsx
|
||||
./src/components/foundation/wallet-session-id/index.ts
|
||||
./src/components/foundation/wallet-session-id/types.ts
|
||||
./src/components/iframe/auto-sign-in/AutoSignInIFrameModal.tsx
|
||||
./src/components/iframe/auto-sign-in/README.md
|
||||
./src/components/iframe/auto-sign-in/index.ts
|
||||
./src/components/iframe/auto-sign-in/types.ts
|
||||
./src/components/iframe/check-balance-iframe/CheckBalanceIframe.tsx
|
||||
./src/components/iframe/check-balance-iframe/useCheckBalance.tsx
|
||||
./src/components/layout/index.ts
|
||||
./src/components/layout/navigation/github-session-button/GitHubSessionButton.tsx
|
||||
./src/components/layout/navigation/github-session-button/README.md
|
||||
./src/components/layout/navigation/github-session-button/index.ts
|
||||
./src/components/layout/navigation/github-session-button/types.ts
|
||||
./src/components/layout/navigation/laconic-icon/LaconicIcon.tsx
|
||||
./src/components/layout/navigation/laconic-icon/README.md
|
||||
./src/components/layout/navigation/laconic-icon/index.ts
|
||||
./src/components/layout/navigation/laconic-icon/types.ts
|
||||
./src/components/layout/navigation/navigation-actions/NavigationActions.tsx
|
||||
./src/components/layout/navigation/navigation-actions/README.md
|
||||
./src/components/layout/navigation/navigation-actions/index.ts
|
||||
./src/components/layout/navigation/navigation-actions/types.ts
|
||||
./src/components/layout/navigation/wallet-session-id/README.md
|
||||
./src/components/layout/navigation/wallet-session-id/WalletSessionId.tsx
|
||||
./src/components/layout/navigation/wallet-session-id/index.ts
|
||||
./src/components/layout/navigation/wallet-session-id/types.ts
|
||||
./src/components/loading/loading-overlay.tsx
|
||||
./src/components/onboarding/OPTIMIZATION.md
|
||||
./src/components/onboarding/Onboarding.tsx
|
||||
./src/components/onboarding/OnboardingButton.tsx
|
||||
./src/components/onboarding/OnboardingDialog.tsx
|
||||
./src/components/onboarding/README.md
|
||||
./src/components/onboarding/common/background-svg.tsx
|
||||
./src/components/onboarding/common/index.ts
|
||||
./src/components/onboarding/common/laconic-icon-lettering.tsx
|
||||
./src/components/onboarding/common/onboarding-container.tsx
|
||||
./src/components/onboarding/common/step-header.tsx
|
||||
./src/components/onboarding/common/step-navigation.tsx
|
||||
./src/components/onboarding/configure-step/configure-step.tsx
|
||||
./src/components/onboarding/configure-step/index.ts
|
||||
./src/components/onboarding/connect-step/connect-button.tsx
|
||||
./src/components/onboarding/connect-step/connect-deploy-first-app.tsx
|
||||
./src/components/onboarding/connect-step/connect-initial.tsx
|
||||
./src/components/onboarding/connect-step/connect-step.tsx
|
||||
./src/components/onboarding/connect-step/index.ts
|
||||
./src/components/onboarding/connect-step/repository-list.tsx
|
||||
./src/components/onboarding/connect-step/template-list.tsx
|
||||
./src/components/onboarding/deploy-step/deploy-step.tsx
|
||||
./src/components/onboarding/deploy-step/index.ts
|
||||
./src/components/onboarding/index.ts
|
||||
./src/components/onboarding/sidebar/index.ts
|
||||
./src/components/onboarding/sidebar/sidebar-nav.tsx
|
||||
./src/components/onboarding/store.ts
|
||||
./src/components/onboarding/types.ts
|
||||
./src/components/onboarding/useOnboarding.ts
|
||||
./src/components/projects/project/ProjectCard/FixedProjectCard.tsx
|
||||
./src/components/projects/project/ProjectCard/ProjectCard.tsx
|
||||
./src/components/projects/project/ProjectCard/ProjectCardActions.tsx
|
||||
./src/components/projects/project/ProjectCard/ProjectDeploymentInfo.tsx
|
||||
./src/components/projects/project/ProjectCard/ProjectStatusDot.tsx
|
||||
./src/components/projects/project/ProjectCard/index.ts
|
||||
./src/components/projects/project/ProjectSearchBar/ProjectSearchBar.tsx
|
||||
./src/components/projects/project/ProjectSearchBar/ProjectSearchBarDialog.tsx
|
||||
./src/components/projects/project/ProjectSearchBar/ProjectSearchBarEmpty.tsx
|
||||
./src/components/projects/project/ProjectSearchBar/ProjectSearchBarItem.tsx
|
||||
./src/components/projects/project/ProjectSearchBar/index.ts
|
||||
./src/components/projects/project/deployments/DeploymentDetailsCard.tsx
|
||||
./src/components/projects/project/deployments/FilterForm.tsx
|
||||
./src/components/projects/project/overview/Activity/AuctionCard.tsx
|
||||
./src/components/projects/project/overview/OverviewInfo.tsx
|
||||
./src/components/providers.tsx
|
||||
./src/context/GQLClientContext.tsx
|
||||
./src/context/OctokitContext.tsx
|
||||
./src/context/OctokitProviderWithRouter.tsx
|
||||
./src/context/WalletContext.tsx
|
||||
./src/context/WalletContextProvider.tsx
|
||||
./src/context/index.ts
|
||||
./src/hooks/useRepoData.tsx
|
||||
./src/lib/utils.ts
|
||||
./src/middleware.ts
|
||||
./src/types/common.ts
|
||||
./src/types/dashboard.ts
|
||||
./src/types/deployment.ts
|
||||
./src/types/hooks/.gitkeep
|
||||
./src/types/hooks/use-mobile.tsx
|
||||
./src/types/index.ts
|
||||
./src/types/project.ts
|
||||
./src/utils/getInitials.ts
|
||||
./src/utils/time.ts
|
||||
./standards/architecture/routes.md
|
||||
./tailwind.config.ts
|
||||
./tsconfig.json
|
||||
@@ -0,0 +1,36 @@
|
||||
// app/actions/github.ts
|
||||
'use server'
|
||||
|
||||
import { auth, currentUser } from '@clerk/nextjs/server'
|
||||
import { Octokit } from '@octokit/rest'
|
||||
import type { Organization } from '@octokit/webhooks-types'
|
||||
|
||||
export async function getGitHubOrgs() {
|
||||
const { userId } = await auth()
|
||||
|
||||
if (!userId) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
const user = await currentUser()
|
||||
const githubAccount = user?.externalAccounts.find(
|
||||
(account) => account.provider === 'github'
|
||||
)
|
||||
|
||||
const token =
|
||||
githubAccount?.provider === 'github' ? githubAccount.externalId : null
|
||||
|
||||
if (!token) {
|
||||
throw new Error('GitHub not connected')
|
||||
}
|
||||
|
||||
const octokit = new Octokit({ auth: token })
|
||||
const { data } = await octokit.rest.orgs.listForAuthenticatedUser()
|
||||
|
||||
return data.map((org: Organization) => ({
|
||||
id: org.id,
|
||||
name: org.login,
|
||||
login: org.login,
|
||||
avatarUrl: org.avatar_url
|
||||
}))
|
||||
}
|
||||
+669
@@ -0,0 +1,669 @@
|
||||
'use client'
|
||||
|
||||
import { Separator } from '@radix-ui/react-dropdown-menu'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Input } from '@workspace/ui/components/input'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetTrigger
|
||||
} from '@workspace/ui/components/sheet'
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger
|
||||
} from '@workspace/ui/components/tabs'
|
||||
import {
|
||||
ChevronDown,
|
||||
Code,
|
||||
Copy,
|
||||
Github,
|
||||
Globe,
|
||||
Menu,
|
||||
Moon,
|
||||
Search,
|
||||
Sun,
|
||||
Terminal,
|
||||
X
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
|
||||
// Add this component after the imports
|
||||
function ComingSoonOverlay({ routerAction }: { routerAction: () => void }) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-background/35 backdrop-blur-sm">
|
||||
<div className="text-center space-y-4 max-w-md px-4 bg-background/80 p-8 rounded-lg shadow-lg">
|
||||
<Globe className="h-16 w-16 mx-auto text-primary" />
|
||||
<h1 className="text-4xl font-bold tracking-tight">Coming Soon</h1>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
Our documentation is currently under development. Check back soon for
|
||||
comprehensive guides and tutorials.
|
||||
</p>
|
||||
<div className="pt-4">
|
||||
<Button size="lg" onClick={() => routerAction()}>
|
||||
Get Notified When We Launch
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function DocumentationPage() {
|
||||
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false)
|
||||
const [isDarkMode, setIsDarkMode] = useState(false)
|
||||
const router = useRouter()
|
||||
const toggleDarkMode = () => {
|
||||
setIsDarkMode(!isDarkMode)
|
||||
document.documentElement.classList.toggle('dark')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen flex flex-col ${isDarkMode ? 'dark' : ''}`}>
|
||||
<ComingSoonOverlay routerAction={() => router.back()} />
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-40 w-full border-b bg-background">
|
||||
<div className="container flex h-16 items-center space-x-4 sm:justify-between sm:space-x-0">
|
||||
<div className="flex gap-6 md:gap-10">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<Globe className="h-6 w-6" />
|
||||
<span className="inline-block font-bold">Laconic Deploy</span>
|
||||
</Link>
|
||||
<nav className="hidden md:flex gap-6">
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center text-sm font-medium text-muted-foreground transition-colors hover:text-primary"
|
||||
>
|
||||
Documentation
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center text-sm font-medium text-muted-foreground transition-colors hover:text-primary"
|
||||
>
|
||||
API Reference
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center text-sm font-medium text-muted-foreground transition-colors hover:text-primary"
|
||||
>
|
||||
Guides
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center text-sm font-medium text-muted-foreground transition-colors hover:text-primary"
|
||||
>
|
||||
Examples
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center space-x-4 sm:justify-end">
|
||||
<div className="flex-1 sm:grow-0">
|
||||
<div className="relative hidden md:block">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search documentation..."
|
||||
className="w-[200px] sm:w-[300px] pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="flex items-center space-x-1">
|
||||
<Button variant="ghost" size="icon" onClick={toggleDarkMode}>
|
||||
{isDarkMode ? (
|
||||
<Sun className="h-5 w-5" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5" />
|
||||
)}
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
<Link href="#" target="_blank" rel="noreferrer">
|
||||
<Button variant="ghost" size="icon">
|
||||
<Github className="h-5 w-5" />
|
||||
<span className="sr-only">GitHub</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Sheet open={isMobileNavOpen} onOpenChange={setIsMobileNavOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="md:hidden">
|
||||
<Menu className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="pr-0">
|
||||
<MobileNav onNavClose={() => setIsMobileNavOpen(false)} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] md:gap-6 lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-10">
|
||||
{/* Sidebar */}
|
||||
<aside className="fixed top-14 z-30 -ml-2 hidden h-[calc(100vh-3.5rem)] w-full shrink-0 md:sticky md:block">
|
||||
<div className="h-full py-6 pl-8 pr-6 lg:py-8">
|
||||
<DocsSidebar />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="relative py-6 lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_300px]">
|
||||
<div className="mx-auto w-full min-w-0">
|
||||
<div className="mb-4 flex items-center space-x-1 text-sm text-muted-foreground">
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
Docs
|
||||
</div>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
<div className="font-medium text-foreground">Getting Started</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="scroll-m-20 text-4xl font-bold tracking-tight">
|
||||
Getting Started with Laconic Deploy
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Learn how to deploy your applications with Laconic Deploy in
|
||||
minutes.
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4 md:my-6" />
|
||||
|
||||
<div className="prose prose-slate dark:prose-invert max-w-none">
|
||||
<p>
|
||||
Laconic Deploy is a modern deployment platform that makes it
|
||||
easy to deploy your applications to the cloud. With Laconic
|
||||
Deploy, you can deploy your applications with just a few clicks
|
||||
or commands.
|
||||
</p>
|
||||
|
||||
<h2>Installation</h2>
|
||||
<p>
|
||||
To get started with Laconic Deploy, you need to install the
|
||||
Laconic CLI. You can install it using npm:
|
||||
</p>
|
||||
|
||||
<div className="relative my-6 overflow-hidden rounded-lg border bg-muted p-2">
|
||||
<div className="flex items-center bg-slate-950 px-4 py-2 text-xs text-slate-50 dark:bg-slate-800">
|
||||
<Terminal className="mr-2 h-4 w-4" />
|
||||
<span>Terminal</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-auto h-8 w-8"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
<span className="sr-only">Copy code</span>
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="p-4 text-sm">
|
||||
<code>npm install -g laconic-cli</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h2>Authentication</h2>
|
||||
<p>
|
||||
After installing the CLI, you need to authenticate with Laconic
|
||||
Deploy:
|
||||
</p>
|
||||
|
||||
<div className="relative my-6 overflow-hidden rounded-lg border bg-muted p-2">
|
||||
<div className="flex items-center bg-slate-950 px-4 py-2 text-xs text-slate-50 dark:bg-slate-800">
|
||||
<Terminal className="mr-2 h-4 w-4" />
|
||||
<span>Terminal</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-auto h-8 w-8"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
<span className="sr-only">Copy code</span>
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="p-4 text-sm">
|
||||
<code>laconic login</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h2>Creating Your First Project</h2>
|
||||
<p>
|
||||
To create a new project, use the <code>laconic init</code>{' '}
|
||||
command:
|
||||
</p>
|
||||
|
||||
<div className="relative my-6 overflow-hidden rounded-lg border bg-muted p-2">
|
||||
<div className="flex items-center bg-slate-950 px-4 py-2 text-xs text-slate-50 dark:bg-slate-800">
|
||||
<Terminal className="mr-2 h-4 w-4" />
|
||||
<span>Terminal</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-auto h-8 w-8"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
<span className="sr-only">Copy code</span>
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="p-4 text-sm">
|
||||
<code>laconic init my-awesome-project</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h2>Deploying Your Application</h2>
|
||||
<p>
|
||||
Once your project is set up, you can deploy it with a single
|
||||
command:
|
||||
</p>
|
||||
|
||||
<div className="relative my-6 overflow-hidden rounded-lg border bg-muted p-2">
|
||||
<div className="flex items-center bg-slate-950 px-4 py-2 text-xs text-slate-50 dark:bg-slate-800">
|
||||
<Terminal className="mr-2 h-4 w-4" />
|
||||
<span>Terminal</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-auto h-8 w-8"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
<span className="sr-only">Copy code</span>
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="p-4 text-sm">
|
||||
<code>laconic deploy</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h2>Configuration</h2>
|
||||
<p>
|
||||
Laconic Deploy uses a <code>laconic.config.js</code> file to
|
||||
configure your deployments. Here's an example configuration:
|
||||
</p>
|
||||
|
||||
<div className="relative my-6 overflow-hidden rounded-lg border bg-muted p-2">
|
||||
<div className="flex items-center bg-slate-950 px-4 py-2 text-xs text-slate-50 dark:bg-slate-800">
|
||||
<Code className="mr-2 h-4 w-4" />
|
||||
<span>laconic.config.js</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-auto h-8 w-8"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
<span className="sr-only">Copy code</span>
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="p-4 text-sm">
|
||||
<code>{`module.exports = {
|
||||
name: 'my-awesome-project',
|
||||
region: 'us-west-1',
|
||||
environment: {
|
||||
NODE_ENV: 'production',
|
||||
API_URL: 'https://api.example.com'
|
||||
},
|
||||
resources: {
|
||||
compute: {
|
||||
type: 'container',
|
||||
size: 'small',
|
||||
port: 3000
|
||||
},
|
||||
database: {
|
||||
type: 'postgres',
|
||||
version: '14'
|
||||
}
|
||||
}
|
||||
}`}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h2>Next Steps</h2>
|
||||
<p>
|
||||
Now that you've deployed your first application, you might want
|
||||
to explore:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>Setting up custom domains</li>
|
||||
<li>Configuring environment variables</li>
|
||||
<li>Setting up CI/CD pipelines</li>
|
||||
<li>Monitoring and logging</li>
|
||||
<li>Scaling your application</li>
|
||||
</ul>
|
||||
|
||||
<div className="not-prose">
|
||||
<Tabs defaultValue="cli" className="w-full my-6">
|
||||
<TabsList className="w-full grid grid-cols-3">
|
||||
<TabsTrigger value="cli">CLI</TabsTrigger>
|
||||
<TabsTrigger value="api">API</TabsTrigger>
|
||||
<TabsTrigger value="dashboard">Dashboard</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="cli" className="p-4 border rounded-b-lg">
|
||||
<p className="text-sm">
|
||||
The Laconic CLI provides a powerful command-line interface
|
||||
for managing your deployments. See the{' '}
|
||||
<Link href="#" className="text-primary hover:underline">
|
||||
CLI Reference
|
||||
</Link>{' '}
|
||||
for more information.
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="api" className="p-4 border rounded-b-lg">
|
||||
<p className="text-sm">
|
||||
The Laconic API allows you to programmatically manage your
|
||||
deployments. See the{' '}
|
||||
<Link href="#" className="text-primary hover:underline">
|
||||
API Reference
|
||||
</Link>{' '}
|
||||
for more information.
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="dashboard"
|
||||
className="p-4 border rounded-b-lg"
|
||||
>
|
||||
<p className="text-sm">
|
||||
The Laconic Dashboard provides a web interface for
|
||||
managing your deployments. Visit the{' '}
|
||||
<Link href="#" className="text-primary hover:underline">
|
||||
Dashboard
|
||||
</Link>{' '}
|
||||
to get started.
|
||||
</p>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-8 border-t pt-4">
|
||||
<Button variant="outline" size="sm">
|
||||
<ChevronDown className="mr-2 h-4 w-4 rotate-90" />
|
||||
Introduction
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Custom Domains
|
||||
<ChevronDown className="ml-2 h-4 w-4 -rotate-90" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table of contents - desktop only */}
|
||||
<div className="hidden text-sm xl:block">
|
||||
<div className="sticky top-16 -mt-10 pt-10">
|
||||
<div className="pb-8">
|
||||
<div className="sticky top-16 -mt-10 pt-10">
|
||||
<h4 className="mb-1 font-medium">On This Page</h4>
|
||||
<ul className="m-0 list-none">
|
||||
<li className="mt-2 pt-2">
|
||||
<a
|
||||
href="#installation"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Installation
|
||||
</a>
|
||||
</li>
|
||||
<li className="mt-2 pt-2">
|
||||
<a
|
||||
href="#authentication"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Authentication
|
||||
</a>
|
||||
</li>
|
||||
<li className="mt-2 pt-2">
|
||||
<a
|
||||
href="#creating-your-first-project"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Creating Your First Project
|
||||
</a>
|
||||
</li>
|
||||
<li className="mt-2 pt-2">
|
||||
<a
|
||||
href="#deploying-your-application"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Deploying Your Application
|
||||
</a>
|
||||
</li>
|
||||
<li className="mt-2 pt-2">
|
||||
<a
|
||||
href="#configuration"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Configuration
|
||||
</a>
|
||||
</li>
|
||||
<li className="mt-2 pt-2">
|
||||
<a
|
||||
href="#next-steps"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Next Steps
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer className="border-t py-6 md:py-0">
|
||||
<div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">
|
||||
<p className="text-center text-sm leading-loose text-muted-foreground md:text-left">
|
||||
© {new Date().getFullYear()} Laconic Deploy. All rights reserved.
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="#"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Terms
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Privacy
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Contact
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileNav({ onNavClose }: { onNavClose: () => void }) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center h-16 px-4 border-b">
|
||||
<Link href="/" className="flex items-center gap-2" onClick={onNavClose}>
|
||||
<Globe className="h-6 w-6" />
|
||||
<span className="font-bold">Laconic Deploy</span>
|
||||
</Link>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-auto"
|
||||
onClick={onNavClose}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto py-4">
|
||||
<div className="px-4 mb-4">
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search documentation..."
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<DocsSidebar mobile onNavClose={onNavClose} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DocsSidebar({
|
||||
mobile = false,
|
||||
onNavClose
|
||||
}: { mobile?: boolean; onNavClose?: () => void }) {
|
||||
const handleLinkClick = () => {
|
||||
if (mobile && onNavClose) {
|
||||
onNavClose()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="pb-4">
|
||||
<h4 className="mb-1 rounded-md px-2 py-1 text-sm font-medium">
|
||||
Getting Started
|
||||
</h4>
|
||||
<div className="grid grid-flow-row auto-rows-max text-sm">
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Introduction
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-foreground font-medium"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Getting Started
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Installation
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
CLI Setup
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pb-4">
|
||||
<h4 className="mb-1 rounded-md px-2 py-1 text-sm font-medium">
|
||||
Core Concepts
|
||||
</h4>
|
||||
<div className="grid grid-flow-row auto-rows-max text-sm">
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Projects
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Environments
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Deployments
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Resources
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pb-4">
|
||||
<h4 className="mb-1 rounded-md px-2 py-1 text-sm font-medium">
|
||||
Guides
|
||||
</h4>
|
||||
<div className="grid grid-flow-row auto-rows-max text-sm">
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Custom Domains
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Environment Variables
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
CI/CD Integration
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Monitoring & Logging
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pb-4">
|
||||
<h4 className="mb-1 rounded-md px-2 py-1 text-sm font-medium">
|
||||
API Reference
|
||||
</h4>
|
||||
<div className="grid grid-flow-row auto-rows-max text-sm">
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Authentication
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Projects API
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Deployments API
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline text-muted-foreground hover:text-foreground"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
Resources API
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Documentation page for the Deploy platform using Pagewrapper component
|
||||
|
||||
import PageWrapper from '@/components/foundation/page-wrapper/PageWrapper'
|
||||
import DocumentationPlaceholder from './DocumentationPlaceholder'
|
||||
|
||||
export default function DocumentationPage() {
|
||||
return (
|
||||
<PageWrapper header={{ title: 'Documentation' }}>
|
||||
<DocumentationPlaceholder />
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
'use client'
|
||||
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingOverlay />
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import { auth, currentUser } from '@clerk/nextjs/server'
|
||||
import { notFound } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { Octokit } from '@octokit/rest'
|
||||
import { Shapes } from 'lucide-react'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
|
||||
/**
|
||||
* Dashboard page
|
||||
* @returns {React.ReactNode} The rendered component
|
||||
*/
|
||||
export default async function Page() {
|
||||
const authenticated = await auth()
|
||||
const userId = authenticated.userId
|
||||
|
||||
if (!userId) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await currentUser()
|
||||
const githubAccount = user?.externalAccounts.find(
|
||||
(account) => account.provider === 'oauth_github'
|
||||
)
|
||||
|
||||
if (!githubAccount) {
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: 'Dashboard',
|
||||
actions: [{ label: 'Create Project', href: '/projects/create' }]
|
||||
}}
|
||||
layout="bento"
|
||||
className="pb-0"
|
||||
>
|
||||
<div className="md:col-span-3 border border-gray-800 rounded-lg min-h-[600px] flex flex-col items-center justify-center p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<Shapes size={64} className="stroke-current" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold mb-2">
|
||||
GitHub Account Not Connected
|
||||
</div>
|
||||
<div className="text-muted-foreground text-center max-w-md mb-6">
|
||||
You need to connect your GitHub account to use the dashboard features.
|
||||
Please visit your user profile in Clerk to connect GitHub.
|
||||
</div>
|
||||
<Button
|
||||
className="bg-white text-black hover:bg-gray-200 flex items-center"
|
||||
asChild
|
||||
>
|
||||
<Link href="https://accounts.clerk.dev/user" target="_blank">
|
||||
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||
</svg>
|
||||
Connect to GitHub
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
// NOTE: We're keeping the token approach for now, but aware it's not working
|
||||
const authToken = githubAccount.accessToken;
|
||||
|
||||
// Try using GitHub token
|
||||
let octokit;
|
||||
try {
|
||||
octokit = new Octokit({
|
||||
auth: authToken || process.env.GITHUB_TOKEN
|
||||
});
|
||||
|
||||
// Test with a simple request
|
||||
|
||||
// Try listing repositories
|
||||
const repoResponse = await octokit.repos.listForAuthenticatedUser();
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: 'Dashboard',
|
||||
actions: [{ label: 'Create Project', href: '/projects/create' }]
|
||||
}}
|
||||
layout="bento"
|
||||
className="pb-0"
|
||||
>
|
||||
<div className="md:col-span-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{repoResponse.data.length > 0 ? (
|
||||
repoResponse.data.map((repo) => (
|
||||
<div
|
||||
key={repo.id}
|
||||
className="border border-gray-800 rounded-lg p-6 flex flex-col"
|
||||
>
|
||||
<h3 className="text-lg font-medium mb-2">{repo.name}</h3>
|
||||
<p className="text-muted-foreground text-sm mb-4 flex-grow">
|
||||
{repo.description || 'No description provided'}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{repo.default_branch}
|
||||
</span>
|
||||
</div>
|
||||
<Link
|
||||
href={repo.html_url}
|
||||
target="_blank"
|
||||
className="text-sm text-blue-400 hover:underline"
|
||||
>
|
||||
View on GitHub
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="md:col-span-3 flex justify-center items-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">No repositories found</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
);
|
||||
} catch (authError) {
|
||||
console.error("GitHub API error:", authError);
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: 'Dashboard',
|
||||
actions: [{ label: 'Create Project', href: '/projects/create' }]
|
||||
}}
|
||||
layout="bento"
|
||||
className="pb-0"
|
||||
>
|
||||
<div className="md:col-span-3 border border-gray-800 rounded-lg min-h-[600px] flex flex-col items-center justify-center p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<Shapes size={64} className="stroke-current" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xl font-bold mb-2">
|
||||
Failed to access GitHub API
|
||||
</div>
|
||||
<div className="text-red-500 mb-4">
|
||||
{authError.message}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-center max-w-md mb-6">
|
||||
<p>This issue may be related to how Clerk is managing the GitHub token.</p>
|
||||
<p className="mt-2">Try reconnecting your GitHub account with the correct permissions.</p>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-white text-black hover:bg-gray-200 flex items-center"
|
||||
asChild
|
||||
>
|
||||
<Link href="https://accounts.clerk.dev/user" target="_blank">
|
||||
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||
</svg>
|
||||
Connect to GitHub
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("GitHub authentication error:", error);
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: 'Dashboard',
|
||||
actions: [{ label: 'Create Project', href: '/projects/create' }]
|
||||
}}
|
||||
layout="bento"
|
||||
className="pb-0"
|
||||
>
|
||||
<div className="md:col-span-3 flex flex-col items-center justify-center min-h-[600px]">
|
||||
<div className="text-2xl font-bold mb-4">
|
||||
Failed to authenticate with GitHub
|
||||
</div>
|
||||
<div className="text-red-500">
|
||||
{error.message}
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import NavigationWrapper from '@/components/foundation/navigation-wrapper/NavigationWrapper'
|
||||
import { TopNavigation } from '@/components/foundation/top-navigation'
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
return (
|
||||
<NavigationWrapper>
|
||||
<TopNavigation />
|
||||
{children}
|
||||
</NavigationWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
'use client'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||
import { Input } from '@workspace/ui/components/input'
|
||||
import { Label } from '@workspace/ui/components/label'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@workspace/ui/components/select'
|
||||
import { toast } from 'sonner'
|
||||
import { Stepper } from '@/components/core/stepper/Stepper'
|
||||
import { useRepoData } from '@/hooks/useRepoData'
|
||||
|
||||
export default function ConfigureDeploymentPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const providerParam = params?.provider ? String(params.provider) : 'github'
|
||||
|
||||
// Use the existing useRepoData hook to fetch all repos (empty string for ID means all repos)
|
||||
const { repoData: repositories, isLoading } = useRepoData('')
|
||||
|
||||
const [selectedRepo, setSelectedRepo] = useState<string>('')
|
||||
const [selectedBranch, setSelectedBranch] = useState<string>('main')
|
||||
const [projectName, setProjectName] = useState<string>('')
|
||||
const [branches, setBranches] = useState<string[]>(['main'])
|
||||
const [envVars, setEnvVars] = useState<{ key: string; value: string }[]>([
|
||||
{ key: '', value: '' }
|
||||
])
|
||||
|
||||
// Define stepper values for the existing Stepper component
|
||||
const stepperValues = [
|
||||
{ step: 1, label: 'Select Repository', route: '/projects/github/ps/cr/tm/cf' },
|
||||
{ step: 2, label: 'Configure', route: '/projects/github/ps/cr/cf' },
|
||||
{ step: 3, label: 'Deploy', route: '/projects/github/ps/cr/dp' },
|
||||
{ step: 4, label: 'Success', route: '/projects/github/ps/cr/sc' }
|
||||
]
|
||||
|
||||
// When a repository is selected, update project name and branch
|
||||
useEffect(() => {
|
||||
if (!selectedRepo || !repositories) return
|
||||
|
||||
const repo = repositories.find(r => r.full_name === selectedRepo)
|
||||
if (repo) {
|
||||
setProjectName(repo.name)
|
||||
setSelectedBranch(repo.default_branch)
|
||||
|
||||
// For simplicity, just use the default branch and some common branch names
|
||||
// In a real implementation, you would fetch branches for the selected repo
|
||||
setBranches([repo.default_branch, 'develop', 'feature/new-ui'])
|
||||
}
|
||||
}, [selectedRepo, repositories])
|
||||
|
||||
const handleRepoChange = (repo: string) => {
|
||||
setSelectedRepo(repo)
|
||||
}
|
||||
|
||||
const handleBranchChange = (branch: string) => {
|
||||
setSelectedBranch(branch)
|
||||
}
|
||||
|
||||
const handleProjectNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setProjectName(e.target.value)
|
||||
}
|
||||
|
||||
const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => {
|
||||
const newEnvVars = [...envVars]
|
||||
newEnvVars[index][field] = value
|
||||
|
||||
// Add a new empty row if the last row has both key and value filled
|
||||
if (
|
||||
index === newEnvVars.length - 1 &&
|
||||
newEnvVars[index].key !== '' &&
|
||||
newEnvVars[index].value !== ''
|
||||
) {
|
||||
newEnvVars.push({ key: '', value: '' })
|
||||
}
|
||||
|
||||
setEnvVars(newEnvVars)
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!selectedRepo || !selectedBranch || !projectName) {
|
||||
toast.error('Please fill in all required fields')
|
||||
return
|
||||
}
|
||||
|
||||
// Filter out empty env vars
|
||||
const filteredEnvVars = envVars.filter(
|
||||
envVar => envVar.key.trim() !== '' && envVar.value.trim() !== ''
|
||||
)
|
||||
|
||||
// Convert env vars array to object
|
||||
const environmentVariables = filteredEnvVars.reduce(
|
||||
(acc, { key, value }) => ({ ...acc, [key]: value }),
|
||||
{}
|
||||
)
|
||||
|
||||
// Find the selected repository to get its URL
|
||||
const repo = repositories?.find(r => r.full_name === selectedRepo)
|
||||
|
||||
// Store the configuration in session storage to be used in the next step
|
||||
sessionStorage.setItem(
|
||||
'deploymentConfig',
|
||||
JSON.stringify({
|
||||
repositoryUrl: selectedRepo,
|
||||
repositoryHtmlUrl: repo?.html_url || `https://github.com/${selectedRepo}`,
|
||||
branch: selectedBranch,
|
||||
projectName,
|
||||
environmentVariables
|
||||
})
|
||||
)
|
||||
|
||||
// Navigate to the deployment page
|
||||
router.push(`/projects/${providerParam}/ps/cr/dp`)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingOverlay isLoading={true} />
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: 'Configure Deployment',
|
||||
description: 'Set up your project deployment configuration'
|
||||
}}
|
||||
>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{/* Using the existing Stepper component with the correct props */}
|
||||
<Stepper activeStep={2} stepperValues={stepperValues} />
|
||||
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Project Configuration</CardTitle>
|
||||
<CardDescription>
|
||||
Configure your project settings for deployment
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="repo">GitHub Repository</Label>
|
||||
<Select
|
||||
value={selectedRepo}
|
||||
onValueChange={handleRepoChange}
|
||||
>
|
||||
<SelectTrigger id="repo">
|
||||
<SelectValue placeholder="Select a repository" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{repositories && repositories.map(repo => (
|
||||
<SelectItem key={repo.id} value={repo.full_name}>
|
||||
{repo.full_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="branch">Branch</Label>
|
||||
<Select
|
||||
value={selectedBranch}
|
||||
onValueChange={handleBranchChange}
|
||||
disabled={!selectedRepo}
|
||||
>
|
||||
<SelectTrigger id="branch">
|
||||
<SelectValue placeholder="Select a branch" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{branches.map(branch => (
|
||||
<SelectItem key={branch} value={branch}>
|
||||
{branch}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="projectName">Project Name</Label>
|
||||
<Input
|
||||
id="projectName"
|
||||
value={projectName}
|
||||
onChange={handleProjectNameChange}
|
||||
placeholder="Enter a name for your project"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Environment Variables</Label>
|
||||
<div className="space-y-2">
|
||||
{envVars.map((envVar, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
placeholder="KEY"
|
||||
value={envVar.key}
|
||||
onChange={e => handleEnvVarChange(index, 'key', e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
placeholder="VALUE"
|
||||
value={envVar.value}
|
||||
onChange={e => handleEnvVarChange(index, 'value', e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Environment variables will be securely stored and available during build and runtime.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline" onClick={() => router.back()}>
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
Continue to Deployment
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
'use client'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
|
||||
import { Stepper } from '@/components/core/stepper/Stepper'
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { toast } from 'sonner'
|
||||
import { Progress } from '@workspace/ui/components/progress'
|
||||
import { Loader2, CheckCircle, AlertCircle, GitBranch } from 'lucide-react'
|
||||
import { StopWatch } from '@/components/core/stop-watch'
|
||||
|
||||
interface DeploymentConfig {
|
||||
repositoryUrl: string;
|
||||
repositoryHtmlUrl: string;
|
||||
branch: string;
|
||||
projectName: string;
|
||||
environmentVariables: Record<string, string>;
|
||||
}
|
||||
|
||||
export default function DeployPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const providerParam = params?.provider ? String(params.provider) : 'github'
|
||||
|
||||
const [deploymentConfig, setDeploymentConfig] = useState<DeploymentConfig | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
const [deploymentStatus, setDeploymentStatus] = useState<'idle' | 'pending' | 'building' | 'ready' | 'error'>('idle')
|
||||
const [deploymentProgress, setDeploymentProgress] = useState<number>(0)
|
||||
const [, setElapsedTime] = useState<number>(0)
|
||||
const [deploymentId, setDeploymentId] = useState<string>('')
|
||||
|
||||
// Define stepper values for the existing Stepper component
|
||||
const stepperValues = [
|
||||
{ step: 1, label: 'Select Repository', route: '/projects/github/ps/cr/tm/cf' },
|
||||
{ step: 2, label: 'Configure', route: '/projects/github/ps/cr/cf' },
|
||||
{ step: 3, label: 'Deploy', route: '/projects/github/ps/cr/dp' },
|
||||
{ step: 4, label: 'Success', route: '/projects/github/ps/cr/sc' }
|
||||
]
|
||||
|
||||
// Load deployment config from session storage
|
||||
useEffect(() => {
|
||||
const storedConfig = sessionStorage.getItem('deploymentConfig')
|
||||
|
||||
if (storedConfig) {
|
||||
setDeploymentConfig(JSON.parse(storedConfig))
|
||||
} else {
|
||||
toast.error('Deployment configuration not found')
|
||||
router.push(`/projects/${providerParam}/ps/cr/cf`)
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}, [router, providerParam])
|
||||
|
||||
// Handle elapsed time updates from StopWatch component
|
||||
const handleTimeUpdate = (time: number) => {
|
||||
setElapsedTime(time)
|
||||
}
|
||||
|
||||
// Simulate deployment process (would connect to your backend in a real implementation)
|
||||
const startDeployment = () => {
|
||||
if (!deploymentConfig) {
|
||||
toast.error('Deployment configuration not found')
|
||||
return
|
||||
}
|
||||
|
||||
setIsDeploying(true)
|
||||
setDeploymentStatus('pending')
|
||||
setDeploymentProgress(10)
|
||||
|
||||
// Simulate deployment steps with timeouts
|
||||
setTimeout(() => {
|
||||
setDeploymentStatus('building')
|
||||
setDeploymentProgress(40)
|
||||
|
||||
setTimeout(() => {
|
||||
// 80% chance of success, 20% chance of failure (for demo purposes)
|
||||
const success = Math.random() < 0.8
|
||||
|
||||
if (success) {
|
||||
setDeploymentStatus('ready')
|
||||
setDeploymentProgress(100)
|
||||
|
||||
// Generate a random ID for the deployment
|
||||
const id = Math.random().toString(36).substring(2, 10)
|
||||
setDeploymentId(id)
|
||||
|
||||
// Store deployment details in session storage
|
||||
const deploymentDetails = {
|
||||
id,
|
||||
url: `https://${deploymentConfig.projectName.toLowerCase().replace(/[^a-z0-9]/g, '-')}.laconic.deploy`,
|
||||
projectId: 'project_' + Math.random().toString(36).substring(2, 10),
|
||||
projectName: deploymentConfig.projectName,
|
||||
status: 'ready',
|
||||
createdAt: new Date().toISOString(),
|
||||
repository: {
|
||||
name: deploymentConfig.repositoryUrl.split('/')[1],
|
||||
url: deploymentConfig.repositoryHtmlUrl || `https://github.com/${deploymentConfig.repositoryUrl}`,
|
||||
branch: deploymentConfig.branch
|
||||
}
|
||||
};
|
||||
|
||||
sessionStorage.setItem('deploymentResult', JSON.stringify(deploymentDetails))
|
||||
|
||||
// Navigate to success page after a short delay
|
||||
setTimeout(() => {
|
||||
router.push(`/projects/${providerParam}/ps/cr/sc/${id}`)
|
||||
}, 2000)
|
||||
} else {
|
||||
setDeploymentStatus('error')
|
||||
setDeploymentProgress(100)
|
||||
}
|
||||
|
||||
setIsDeploying(false)
|
||||
}, 5000) // 5 seconds for building
|
||||
}, 3000) // 3 seconds for pending
|
||||
}
|
||||
|
||||
const getStatusIcon = () => {
|
||||
switch (deploymentStatus) {
|
||||
case 'pending':
|
||||
return <Loader2 className="h-6 w-6 animate-spin text-blue-500" />
|
||||
case 'building':
|
||||
return <Loader2 className="h-6 w-6 animate-spin text-blue-500" />
|
||||
case 'ready':
|
||||
return <CheckCircle className="h-6 w-6 text-green-500" />
|
||||
case 'error':
|
||||
return <AlertCircle className="h-6 w-6 text-red-500" />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = () => {
|
||||
switch (deploymentStatus) {
|
||||
case 'pending':
|
||||
return 'Preparing deployment...'
|
||||
case 'building':
|
||||
return 'Building your project...'
|
||||
case 'ready':
|
||||
return 'Deployment successful!'
|
||||
case 'error':
|
||||
return 'Deployment failed'
|
||||
default:
|
||||
return 'Ready to deploy'
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingOverlay isLoading={true} />
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: 'Deploy Project',
|
||||
description: 'Deploy your project to Laconic'
|
||||
}}
|
||||
>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{/* Using the existing Stepper component with the correct props */}
|
||||
<Stepper activeStep={3} stepperValues={stepperValues} />
|
||||
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Deployment</CardTitle>
|
||||
<CardDescription>
|
||||
Deploy your project to Laconic's decentralized hosting
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{deploymentConfig && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Repository</p>
|
||||
<div className="flex items-center">
|
||||
<GitBranch className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{deploymentConfig.repositoryUrl} ({deploymentConfig.branch})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Project Name</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{deploymentConfig.projectName}
|
||||
</p>
|
||||
</div>
|
||||
{Object.keys(deploymentConfig.environmentVariables || {}).length > 0 && (
|
||||
<div>
|
||||
<p className="text-sm font-medium">Environment Variables</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{Object.keys(deploymentConfig.environmentVariables).length} environment variables configured
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{deploymentStatus === 'idle' ? (
|
||||
<div className="flex flex-col items-center justify-center py-6">
|
||||
<p className="text-center mb-4">
|
||||
Ready to deploy your project? Click the button below to start the deployment process.
|
||||
</p>
|
||||
<Button
|
||||
onClick={startDeployment}
|
||||
disabled={isDeploying}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{isDeploying ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Deploying...
|
||||
</>
|
||||
) : (
|
||||
'Start Deployment'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
{getStatusIcon()}
|
||||
<p className="text-sm font-medium">{getStatusText()}</p>
|
||||
</div>
|
||||
{/* Using your existing StopWatch component */}
|
||||
<StopWatch
|
||||
start={deploymentStatus !== 'ready' && deploymentStatus !== 'error'}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
/>
|
||||
</div>
|
||||
<Progress value={deploymentProgress} className="h-2" />
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
{deploymentStatus === 'pending' && 'Setting up the deployment environment...'}
|
||||
{deploymentStatus === 'building' && 'Building your application...'}
|
||||
{deploymentStatus === 'ready' && 'Deployment completed successfully!'}
|
||||
{deploymentStatus === 'error' && 'There was an error deploying your application. Please try again.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => router.back()}
|
||||
disabled={deploymentStatus === 'pending' || deploymentStatus === 'building'}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
{deploymentStatus === 'ready' && (
|
||||
<Button
|
||||
onClick={() => router.push(`/projects/${providerParam}/ps/cr/sc/${deploymentId}`)}
|
||||
>
|
||||
View Deployment
|
||||
</Button>
|
||||
)}
|
||||
{deploymentStatus === 'error' && (
|
||||
<Button onClick={startDeployment}>
|
||||
Retry Deployment
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
'use client'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
|
||||
import { Stepper } from '@/components/core/stepper/Stepper'
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { toast } from 'sonner'
|
||||
import { CheckCircle, Copy, ExternalLink, Clock } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { relativeTimeMs } from '@/utils/time'
|
||||
import { getInitials } from '@/utils/getInitials'
|
||||
import { Avatar, AvatarFallback } from '@workspace/ui/components/avatar'
|
||||
|
||||
interface DeploymentDetails {
|
||||
id: string;
|
||||
url: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
repository: {
|
||||
name: string;
|
||||
url: string;
|
||||
branch: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function SuccessPage({ params }: { params: { id: string } }) {
|
||||
const router = useRouter()
|
||||
const paramsObj = useParams()
|
||||
const providerParam = paramsObj?.provider ? String(paramsObj.provider) : 'github'
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [deployment, setDeployment] = useState<DeploymentDetails | null>(null)
|
||||
const deploymentId = params.id
|
||||
|
||||
// Define stepper values for the existing Stepper component
|
||||
const stepperValues = [
|
||||
{ step: 1, label: 'Select Repository', route: '/projects/github/ps/cr/tm/cf' },
|
||||
{ step: 2, label: 'Configure', route: '/projects/github/ps/cr/cf' },
|
||||
{ step: 3, label: 'Deploy', route: '/projects/github/ps/cr/dp' },
|
||||
{ step: 4, label: 'Success', route: '/projects/github/ps/cr/sc' }
|
||||
]
|
||||
|
||||
// Get deployment details from session storage
|
||||
useEffect(() => {
|
||||
// For now, we'll get the deployment details from session storage
|
||||
// In a real app, you'd fetch this from your API
|
||||
const storedDeployment = sessionStorage.getItem('deploymentResult')
|
||||
|
||||
if (storedDeployment) {
|
||||
setDeployment(JSON.parse(storedDeployment))
|
||||
} else {
|
||||
// If not found in session storage, simulate it (for demo purposes)
|
||||
// In a real app, you'd fetch from the API using the ID
|
||||
simulateDeploymentDetails()
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}, [deploymentId])
|
||||
|
||||
// Simulate deployment details if needed (for demo purposes)
|
||||
const simulateDeploymentDetails = () => {
|
||||
const mockDeployment: DeploymentDetails = {
|
||||
id: deploymentId,
|
||||
url: `https://project-${deploymentId}.laconic.deploy`,
|
||||
projectId: 'project_' + Math.random().toString(36).substring(2, 10),
|
||||
projectName: 'Demo Project',
|
||||
status: 'ready',
|
||||
createdAt: new Date().toISOString(),
|
||||
repository: {
|
||||
name: 'demo-repo',
|
||||
url: 'https://github.com/yourusername/demo-repo',
|
||||
branch: 'main'
|
||||
}
|
||||
}
|
||||
|
||||
setDeployment(mockDeployment)
|
||||
}
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
toast.success('Copied to clipboard')
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingOverlay isLoading={true} />
|
||||
}
|
||||
|
||||
if (!deployment) {
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: 'Deployment Not Found',
|
||||
description: 'The deployment you are looking for does not exist'
|
||||
}}
|
||||
>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<p className="text-center mb-6">
|
||||
We couldn't find the deployment you're looking for. It may have been deleted or expired.
|
||||
</p>
|
||||
<Button asChild>
|
||||
<Link href="/projects">View All Projects</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate relative time for the deployment
|
||||
const deploymentTime = new Date(deployment.createdAt).getTime()
|
||||
const deployedBy = 'You' // In a real app, you'd get this from the deployment data
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: 'Deployment Success',
|
||||
description: 'Your project has been successfully deployed',
|
||||
actions: [
|
||||
{
|
||||
label: 'View App',
|
||||
href: deployment.url,
|
||||
icon: 'external-link',
|
||||
external: true
|
||||
}
|
||||
]
|
||||
}}
|
||||
>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{/* Using the existing Stepper component with the correct props */}
|
||||
<Stepper activeStep={4} stepperValues={stepperValues} />
|
||||
|
||||
<Card className="mt-6">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="h-6 w-6 text-green-500" />
|
||||
<CardTitle>Deployment Successful</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Your project has been successfully deployed to Laconic's decentralized hosting
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="p-4 border rounded-lg bg-muted/50 relative">
|
||||
<h3 className="font-medium mb-2">Deployment URL</h3>
|
||||
<div className="flex items-center">
|
||||
<code className="text-sm bg-background p-2 rounded flex-1 overflow-x-auto">
|
||||
{deployment.url}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => copyToClipboard(deployment.url)}
|
||||
className="ml-2"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2"
|
||||
asChild
|
||||
>
|
||||
<a href={deployment.url} target="_blank" rel="noopener noreferrer">
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h3 className="font-medium mb-2">Project Details</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>
|
||||
<span className="text-muted-foreground">Project Name:</span>{' '}
|
||||
{deployment.projectName}
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-muted-foreground">Repository:</span>{' '}
|
||||
{deployment.repository.name}
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-muted-foreground">Branch:</span>{' '}
|
||||
{deployment.repository.branch}
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-muted-foreground">Deployment ID:</span>{' '}
|
||||
<code className="text-xs bg-muted p-1 rounded">{deployment.id}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-2">Deployment Information</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>
|
||||
<span className="text-muted-foreground">Status:</span>{' '}
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100">
|
||||
{deployment.status.toUpperCase()}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Deployed at:</span>{' '}
|
||||
<span className="ml-1">{relativeTimeMs(deploymentTime)}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="flex items-center">
|
||||
<span className="text-muted-foreground">Deployed by:</span>{' '}
|
||||
<span className="flex items-center ml-1">
|
||||
<Avatar className="h-5 w-5 mr-1">
|
||||
<AvatarFallback>{getInitials(deployedBy)}</AvatarFallback>
|
||||
</Avatar>
|
||||
{deployedBy}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-lg bg-background relative">
|
||||
<h3 className="font-medium mb-2">What's Next?</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>• Configure custom domains for your deployment</li>
|
||||
<li>• Set up automatic deployments for new commits</li>
|
||||
<li>• Add collaborators to your project</li>
|
||||
<li>• Monitor deployment performance and analytics</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => router.push('/projects')}
|
||||
>
|
||||
View All Projects
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => router.push(`/projects/${providerParam}/ps/${deployment.projectId}`)}
|
||||
>
|
||||
Go to Project Dashboard
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>
|
||||
Hello from
|
||||
(web3-authenticated)/(dashboard)/(projects)/pr/[provider]/[orgSlug]/(projects)/ps/(create)/cr/(template)/tm/(configure)/cf
|
||||
</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<PageWrapper header={{ title: 'Deploy Project' }}>
|
||||
<div>
|
||||
<h1>
|
||||
Hello from
|
||||
(web3-authenticated)/(dashboard)/(projects)/pr/[provider]/[orgSlug]/(projects)/ps/(create)/cr/(template)/tm/(deploy)/dp
|
||||
</h1>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { PageWrapper } from '@/components/foundation';
|
||||
import { DeploymentDetailsCard } from '@/components/projects/project/deployments/DeploymentDetailsCard';
|
||||
import { FilterForm } from '@/components/projects/project/deployments/FilterForm';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { IconButton } from '@workspace/ui/components/button';
|
||||
import { Rocket } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRepoData } from '@/hooks/useRepoData';
|
||||
import type { Deployment, Domain } from '@/types';
|
||||
|
||||
export default function DeploymentsPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
// Safely unwrap params
|
||||
const id = params?.id ? String(params.id) : '';
|
||||
const provider = params?.provider ? String(params.provider) : '';
|
||||
|
||||
// Use the hook to get repo data
|
||||
const { repoData } = useRepoData(id);
|
||||
|
||||
// Mock deployments data - in a real app, you would fetch this from an API
|
||||
const [deployments, setDeployments] = useState<Deployment[]>([]);
|
||||
const [filteredDeployments, setFilteredDeployments] = useState<Deployment[]>([]);
|
||||
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
|
||||
|
||||
// Create a default deployment
|
||||
const defaultDeployment: Deployment = {
|
||||
id: 'default',
|
||||
branch: 'main',
|
||||
status: 'COMPLETED',
|
||||
isCurrent: true,
|
||||
createdAt: Date.now() - 24 * 60 * 60 * 1000, // 1 day ago
|
||||
applicationDeploymentRecordData: {
|
||||
url: repoData ? `https://${repoData.name.toLowerCase()}.example.com` : 'https://example.com'
|
||||
},
|
||||
createdBy: {
|
||||
name: repoData?.owner?.login || 'username'
|
||||
}
|
||||
};
|
||||
|
||||
const secondDeployment: Deployment = {
|
||||
id: 'previous',
|
||||
branch: 'feature/new-ui',
|
||||
status: 'COMPLETED',
|
||||
isCurrent: false,
|
||||
createdAt: Date.now() - 3 * 24 * 60 * 60 * 1000, // 3 days ago
|
||||
applicationDeploymentRecordData: {
|
||||
url: repoData ? `https://dev.${repoData.name.toLowerCase()}.example.com` : 'https://dev.example.com'
|
||||
},
|
||||
createdBy: {
|
||||
name: repoData?.owner?.login || 'username'
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize with mock data
|
||||
useEffect(() => {
|
||||
const mockDeployments = [defaultDeployment, secondDeployment];
|
||||
setDeployments(mockDeployments);
|
||||
setFilteredDeployments(mockDeployments);
|
||||
|
||||
// Mock domains
|
||||
const mockDomains: Domain[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: repoData ? `${repoData.name.toLowerCase()}.example.com` : 'example.com',
|
||||
status: 'ACTIVE',
|
||||
isCustom: false
|
||||
}
|
||||
];
|
||||
setProdBranchDomains(mockDomains);
|
||||
}, [repoData]);
|
||||
|
||||
// Handle tab changes by navigating to the correct folder
|
||||
const handleTabChange = (value: string) => {
|
||||
const basePath = `/projects/${provider}/ps/${id}`;
|
||||
|
||||
switch (value) {
|
||||
case 'overview':
|
||||
router.push(basePath);
|
||||
break;
|
||||
case 'deployment':
|
||||
router.push(`${basePath}/dep`);
|
||||
break;
|
||||
case 'settings':
|
||||
router.push(`${basePath}/set`);
|
||||
break;
|
||||
case 'git':
|
||||
router.push(`${basePath}/int`);
|
||||
break;
|
||||
case 'env-vars':
|
||||
router.push(`${basePath}/set/env`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Reset filters handler
|
||||
const handleResetFilters = () => {
|
||||
setFilteredDeployments(deployments);
|
||||
};
|
||||
|
||||
const project = {
|
||||
id: id,
|
||||
prodBranch: 'main',
|
||||
name: repoData?.name || 'Project'
|
||||
};
|
||||
|
||||
const currentDeployment = deployments.find(deployment => deployment.isCurrent) || defaultDeployment;
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: repoData ? `${repoData.name}` : 'Project Deployments',
|
||||
actions: [
|
||||
{
|
||||
label: 'Open repo',
|
||||
href: repoData?.html_url || '#',
|
||||
icon: 'external-link',
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: 'View app',
|
||||
href: currentDeployment.applicationDeploymentRecordData.url,
|
||||
icon: 'external-link',
|
||||
external: true
|
||||
}
|
||||
]
|
||||
}}
|
||||
layout="bento"
|
||||
className="pb-0"
|
||||
>
|
||||
<div className="md:col-span-3 w-full">
|
||||
{/* Tabs navigation */}
|
||||
<Tabs defaultValue="deployment" className="w-full" onValueChange={handleTabChange}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="deployment">Deployment</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
<TabsTrigger value="git">Git</TabsTrigger>
|
||||
<TabsTrigger value="env-vars">Environment Variables</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-6">
|
||||
<FilterForm />
|
||||
<div className="h-full mt-4">
|
||||
{filteredDeployments.length > 0 ? (
|
||||
filteredDeployments.map((deployment) => (
|
||||
<DeploymentDetailsCard
|
||||
key={deployment.id}
|
||||
deployment={deployment}
|
||||
currentDeployment={currentDeployment}
|
||||
project={project}
|
||||
prodBranchDomains={prodBranchDomains}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="h-96 bg-base-bg-alternate dark:bg-overlay3 rounded-xl flex flex-col items-center justify-center gap-5 text-center">
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium tracking-[-0.011em] text-elements-high-em dark:text-foreground">
|
||||
No deployments found
|
||||
</p>
|
||||
<p className="text-sm tracking-[-0.006em] text-elements-mid-em dark:text-foreground-secondary">
|
||||
Please change your search query or filters.
|
||||
</p>
|
||||
</div>
|
||||
<IconButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
leftIcon={<Rocket className="w-4 h-4" />}
|
||||
onClick={handleResetFilters}
|
||||
>
|
||||
RESET FILTERS
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay";
|
||||
|
||||
interface SwitchProps {
|
||||
id: string;
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function Switch({ id, checked, onChange, disabled = false }: SwitchProps) {
|
||||
return (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={`relative inline-flex items-center cursor-pointer ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
className="sr-only"
|
||||
checked={checked}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div
|
||||
className={`relative w-11 h-6 bg-gray-800 rounded-full transition-colors
|
||||
${checked ? 'bg-blue-600' : 'bg-gray-700'}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute w-4 h-4 bg-white rounded-full transition-transform transform
|
||||
${checked ? 'translate-x-6' : 'translate-x-1'} top-1`}
|
||||
></div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GitPage() {
|
||||
const params = useParams();
|
||||
const { provider, id } = params;
|
||||
|
||||
const [pullRequestComments, setPullRequestComments] = useState(true);
|
||||
const [commitComments, setCommitComments] = useState(false);
|
||||
const [productionBranch, setProductionBranch] = useState("main");
|
||||
const [webhookUrl, setWebhookUrl] = useState("");
|
||||
const [isSavingBranch, setIsSavingBranch] = useState(false);
|
||||
const [isSavingWebhook, setIsSavingWebhook] = useState(false);
|
||||
|
||||
const handleSaveBranch = async () => {
|
||||
try {
|
||||
setIsSavingBranch(true);
|
||||
// Save production branch
|
||||
console.log("Saving production branch:", productionBranch);
|
||||
// Implement API call to save production branch
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
|
||||
|
||||
// Show success notification
|
||||
} catch (error) {
|
||||
console.error("Failed to save production branch:", error);
|
||||
// Show error notification
|
||||
} finally {
|
||||
setIsSavingBranch(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveWebhook = async () => {
|
||||
try {
|
||||
setIsSavingWebhook(true);
|
||||
// Save webhook URL
|
||||
console.log("Saving webhook URL:", webhookUrl);
|
||||
// Implement API call to save webhook URL
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
|
||||
|
||||
// Show success notification
|
||||
} catch (error) {
|
||||
console.error("Failed to save webhook URL:", error);
|
||||
// Show error notification
|
||||
} finally {
|
||||
setIsSavingWebhook(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isSavingBranch || isSavingWebhook) && <LoadingOverlay />}
|
||||
|
||||
<div className="space-y-8 w-full">
|
||||
<div className="rounded-lg border border-gray-800 p-6 bg-black">
|
||||
<h2 className="text-xl font-semibold mb-4">Git repository</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Switch
|
||||
id="pull-request-comments"
|
||||
checked={pullRequestComments}
|
||||
onChange={setPullRequestComments}
|
||||
/>
|
||||
<label htmlFor="pull-request-comments" className="text-sm font-medium">
|
||||
Pull request comments
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mt-1 ml-14">
|
||||
Laconic will comment on pull requests opened against this project.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Switch
|
||||
id="commit-comments"
|
||||
checked={commitComments}
|
||||
onChange={setCommitComments}
|
||||
/>
|
||||
<label htmlFor="commit-comments" className="text-sm font-medium">
|
||||
Commit comments
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mt-1 ml-14">
|
||||
Laconic will comment on commits deployed to production.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-800 p-6 bg-black">
|
||||
<h2 className="text-xl font-semibold mb-4">Production branch</h2>
|
||||
|
||||
<p className="text-sm text-gray-400 mb-4">
|
||||
By default, each commit pushed to the main branch initiates a production deployment. You can opt for a
|
||||
different branch for deployment in the settings.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="branch-name" className="block text-sm font-medium mb-1">
|
||||
Branch name
|
||||
</label>
|
||||
<input
|
||||
id="branch-name"
|
||||
value={productionBranch}
|
||||
onChange={(e) => setProductionBranch(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="px-4 py-2 border border-gray-600 rounded-md hover:bg-gray-800 transition-colors"
|
||||
onClick={handleSaveBranch}
|
||||
disabled={isSavingBranch}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-800 p-6 bg-black">
|
||||
<h2 className="text-xl font-semibold mb-4">Deploy webhooks</h2>
|
||||
|
||||
<p className="text-sm text-gray-400 mb-4">
|
||||
Webhooks configured to trigger when there is a change in a project's build or deployment status.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="webhook-url" className="block text-sm font-medium mb-1">
|
||||
Webhook URL
|
||||
</label>
|
||||
<div className="flex">
|
||||
<input
|
||||
id="webhook-url"
|
||||
value={webhookUrl}
|
||||
onChange={(e) => setWebhookUrl(e.target.value)}
|
||||
placeholder="https://"
|
||||
className="flex-1 px-3 py-2 rounded-l-md bg-gray-900 border border-gray-700 text-white"
|
||||
/>
|
||||
<button
|
||||
className="px-4 py-2 border border-gray-600 border-l-0 rounded-r-md hover:bg-gray-800 transition-colors"
|
||||
onClick={handleSaveWebhook}
|
||||
disabled={isSavingWebhook}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { PageWrapper } from "@/components/foundation";
|
||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import GitPage from "./GitPage";
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRepoData } from '@/hooks/useRepoData';
|
||||
|
||||
export default function GitIntegrationsPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
const provider = params.provider as string;
|
||||
// Use the hook to get repo data
|
||||
const { repoData } = useRepoData(id);
|
||||
|
||||
// Handle tab changes by navigating to the correct folder
|
||||
const handleTabChange = (value: string) => {
|
||||
const basePath = `/projects/${provider}/ps/${id}`;
|
||||
|
||||
switch (value) {
|
||||
case 'overview':
|
||||
router.push(basePath);
|
||||
break;
|
||||
case 'deployment':
|
||||
router.push(`${basePath}/dep`);
|
||||
break;
|
||||
case 'settings':
|
||||
router.push(`${basePath}/set`);
|
||||
break;
|
||||
case 'git':
|
||||
router.push(`${basePath}/int`);
|
||||
break;
|
||||
case 'env-vars':
|
||||
router.push(`${basePath}/set/env`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: repoData ? `${repoData.name}` : 'Project Settings',
|
||||
actions: [
|
||||
{
|
||||
label: 'Open repo',
|
||||
href: repoData?.html_url || '#',
|
||||
icon: 'external-link',
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: 'View app',
|
||||
href: repoData ? `https://${repoData.name.toLowerCase()}.example.com` : '#',
|
||||
icon: 'external-link',
|
||||
external: true
|
||||
}
|
||||
]
|
||||
}}
|
||||
layout="bento"
|
||||
className="pb-0"
|
||||
>
|
||||
<div className="md:col-span-3 w-full">
|
||||
{/* Tabs navigation */}
|
||||
<Tabs defaultValue="git" className="w-full" onValueChange={handleTabChange}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="deployment">Deployment</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
<TabsTrigger value="git">Git</TabsTrigger>
|
||||
<TabsTrigger value="env-vars">Environment Variables</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{/* Git content */}
|
||||
<div className="mt-6">
|
||||
<GitPage />
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
'use client'
|
||||
import { PageWrapper } from "@/components/foundation"
|
||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
id: string
|
||||
provider: string
|
||||
orgSlug: string
|
||||
}
|
||||
}
|
||||
|
||||
const Page = ({ params }: PageProps) => {
|
||||
const router = useRouter();
|
||||
// Mock data for the project
|
||||
|
||||
// Handle tab changes by navigating to the correct folder
|
||||
const handleTabChange = (value: string) => {
|
||||
const basePath = `/projects/${params.provider}/ps/${params.id}`;
|
||||
|
||||
switch (value) {
|
||||
case 'overview':
|
||||
router.push(basePath);
|
||||
break;
|
||||
case 'deployment':
|
||||
router.push(`${basePath}/dep`);
|
||||
break;
|
||||
case 'settings':
|
||||
router.push(`${basePath}/set`);
|
||||
break;
|
||||
case 'git':
|
||||
router.push(`${basePath}/int`);
|
||||
break;
|
||||
case 'env-vars':
|
||||
router.push(`${basePath}/set/env`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: `Project Name`,
|
||||
actions: [{ label: 'Open repo', href: '/projects/create' }, { label: 'View app', href: '/projects/create' }]
|
||||
}}
|
||||
layout="bento" // Use bento layout to override max width
|
||||
className="pb-0"
|
||||
>
|
||||
<div className="md:col-span-3 w-full"> {/* Take full width in bento grid */}
|
||||
{/* Tabs navigation */}
|
||||
<Tabs defaultValue="overview" className="w-full" onValueChange={handleTabChange}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="deployment">Deployment</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
<TabsTrigger value="git">Git</TabsTrigger>
|
||||
<TabsTrigger value="env-vars">Environment Variables</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>
|
||||
Hello from
|
||||
(web3-authenticated)/(dashboard)/(projects)/pr/[provider]/[orgSlug]/(projects)/ps/[id]/(settings)/set/(domains)/dom/(add)/cf
|
||||
</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>
|
||||
Hello from
|
||||
(web3-authenticated)/(dashboard)/(projects)/pr/[provider]/[orgSlug]/(projects)/ps/[id]/(settings)/set/(domains)/dom/(add)/config/cf
|
||||
</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
+415
@@ -0,0 +1,415 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay";
|
||||
import { PlusIcon, ChevronDownIcon, ChevronUpIcon, PencilIcon, TrashIcon } from "lucide-react";
|
||||
|
||||
interface EnvVarItem {
|
||||
key: string;
|
||||
value: string;
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
interface EnvGroupProps {
|
||||
title: string;
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
children: React.ReactNode;
|
||||
varCount: number;
|
||||
}
|
||||
|
||||
function EnvGroup({ title, isOpen, onToggle, children, varCount }: EnvGroupProps) {
|
||||
return (
|
||||
<div className="border-b border-gray-800 last:border-b-0">
|
||||
<div
|
||||
className="flex items-center justify-between py-4 cursor-pointer"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="text-lg font-medium">{title}</h3>
|
||||
<span className="text-sm text-gray-400">({varCount})</span>
|
||||
</div>
|
||||
<button className="p-1">
|
||||
{isOpen ? <ChevronUpIcon size={18} /> : <ChevronDownIcon size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="pb-4">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EnvVarsPage() {
|
||||
const params = useParams();
|
||||
const { provider, id } = params;
|
||||
|
||||
const [isAddingVar, setIsAddingVar] = useState(false);
|
||||
const [newVarKey, setNewVarKey] = useState("");
|
||||
const [newVarValue, setNewVarValue] = useState("");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Group states
|
||||
const [productionOpen, setProductionOpen] = useState(true);
|
||||
const [previewOpen, setPreviewOpen] = useState(true);
|
||||
const [deploymentOpen, setDeploymentOpen] = useState(true);
|
||||
|
||||
// Environment variables
|
||||
const [productionVars, setProductionVars] = useState<EnvVarItem[]>([]);
|
||||
const [previewVars, setPreviewVars] = useState<EnvVarItem[]>([
|
||||
{ key: "TEST_KEY", value: "1" }
|
||||
]);
|
||||
const [deploymentVars, setDeploymentVars] = useState<EnvVarItem[]>([]);
|
||||
|
||||
// Checkboxes for environment selection
|
||||
const [envSelection, setEnvSelection] = useState({
|
||||
production: true,
|
||||
preview: true,
|
||||
development: true
|
||||
});
|
||||
|
||||
const handleEnvSelectionChange = (env: 'production' | 'preview' | 'development') => {
|
||||
setEnvSelection({
|
||||
...envSelection,
|
||||
[env]: !envSelection[env]
|
||||
});
|
||||
};
|
||||
|
||||
const addVariable = () => {
|
||||
if (!newVarKey.trim() || !newVarValue.trim()) return;
|
||||
|
||||
const newVar = { key: newVarKey, value: newVarValue };
|
||||
|
||||
if (envSelection.production) {
|
||||
setProductionVars([...productionVars, { ...newVar }]);
|
||||
}
|
||||
|
||||
if (envSelection.preview) {
|
||||
setPreviewVars([...previewVars, { ...newVar }]);
|
||||
}
|
||||
|
||||
if (envSelection.development) {
|
||||
setDeploymentVars([...deploymentVars, { ...newVar }]);
|
||||
}
|
||||
|
||||
// Reset form
|
||||
setNewVarKey("");
|
||||
setNewVarValue("");
|
||||
setIsAddingVar(false);
|
||||
};
|
||||
|
||||
const cancelAddVariable = () => {
|
||||
setNewVarKey("");
|
||||
setNewVarValue("");
|
||||
setIsAddingVar(false);
|
||||
};
|
||||
|
||||
const removeVariable = (env: 'production' | 'preview' | 'development', index: number) => {
|
||||
if (env === 'production') {
|
||||
setProductionVars(productionVars.filter((_, i) => i !== index));
|
||||
} else if (env === 'preview') {
|
||||
setPreviewVars(previewVars.filter((_, i) => i !== index));
|
||||
} else if (env === 'development') {
|
||||
setDeploymentVars(deploymentVars.filter((_, i) => i !== index));
|
||||
}
|
||||
};
|
||||
|
||||
const editVariable = (env: 'production' | 'preview' | 'development', index: number) => {
|
||||
if (env === 'production') {
|
||||
const updatedVars = [...productionVars];
|
||||
updatedVars[index] = { ...updatedVars[index], isEditing: true };
|
||||
setProductionVars(updatedVars);
|
||||
} else if (env === 'preview') {
|
||||
const updatedVars = [...previewVars];
|
||||
updatedVars[index] = { ...updatedVars[index], isEditing: true };
|
||||
setPreviewVars(updatedVars);
|
||||
} else if (env === 'development') {
|
||||
const updatedVars = [...deploymentVars];
|
||||
updatedVars[index] = { ...updatedVars[index], isEditing: true };
|
||||
setDeploymentVars(updatedVars);
|
||||
}
|
||||
};
|
||||
|
||||
const updateVariable = (env: 'production' | 'preview' | 'development', index: number, key: string, value: string) => {
|
||||
if (env === 'production') {
|
||||
const updatedVars = [...productionVars];
|
||||
updatedVars[index] = { key, value, isEditing: false };
|
||||
setProductionVars(updatedVars);
|
||||
} else if (env === 'preview') {
|
||||
const updatedVars = [...previewVars];
|
||||
updatedVars[index] = { key, value, isEditing: false };
|
||||
setPreviewVars(updatedVars);
|
||||
} else if (env === 'development') {
|
||||
const updatedVars = [...deploymentVars];
|
||||
updatedVars[index] = { key, value, isEditing: false };
|
||||
setDeploymentVars(updatedVars);
|
||||
}
|
||||
};
|
||||
|
||||
const saveChanges = async () => {
|
||||
try {
|
||||
setIsSaving(true);
|
||||
// Save environment variables
|
||||
console.log("Saving environment variables:", {
|
||||
production: productionVars,
|
||||
preview: previewVars,
|
||||
deployment: deploymentVars
|
||||
});
|
||||
// Implement API call to save environment variables
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
|
||||
|
||||
// Show success notification
|
||||
} catch (error) {
|
||||
console.error("Failed to save environment variables:", error);
|
||||
// Show error notification
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderEnvVarRow = (env: 'production' | 'preview' | 'development', variable: EnvVarItem, index: number) => {
|
||||
if (variable.isEditing) {
|
||||
return (
|
||||
<div key={index} className="flex items-center space-x-2 mb-2">
|
||||
<input
|
||||
className="flex-1 px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
||||
value={variable.key}
|
||||
onChange={(e) => {
|
||||
const updatedVars = env === 'production'
|
||||
? [...productionVars]
|
||||
: env === 'preview'
|
||||
? [...previewVars]
|
||||
: [...deploymentVars];
|
||||
updatedVars[index] = { ...updatedVars[index], key: e.target.value };
|
||||
if (env === 'production') setProductionVars(updatedVars);
|
||||
else if (env === 'preview') setPreviewVars(updatedVars);
|
||||
else setDeploymentVars(updatedVars);
|
||||
}}
|
||||
placeholder="KEY"
|
||||
/>
|
||||
<input
|
||||
className="flex-1 px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
||||
value={variable.value}
|
||||
onChange={(e) => {
|
||||
const updatedVars = env === 'production'
|
||||
? [...productionVars]
|
||||
: env === 'preview'
|
||||
? [...previewVars]
|
||||
: [...deploymentVars];
|
||||
updatedVars[index] = { ...updatedVars[index], value: e.target.value };
|
||||
if (env === 'production') setProductionVars(updatedVars);
|
||||
else if (env === 'preview') setPreviewVars(updatedVars);
|
||||
else setDeploymentVars(updatedVars);
|
||||
}}
|
||||
placeholder="Value"
|
||||
/>
|
||||
<button
|
||||
className="p-2 hover:bg-gray-800 rounded-md"
|
||||
onClick={() => updateVariable(env, index, variable.key, variable.value)}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
className="p-2 hover:bg-gray-800 rounded-md"
|
||||
onClick={() => {
|
||||
const updatedVars = env === 'production'
|
||||
? [...productionVars]
|
||||
: env === 'preview'
|
||||
? [...previewVars]
|
||||
: [...deploymentVars];
|
||||
updatedVars[index] = { ...updatedVars[index], isEditing: false };
|
||||
if (env === 'production') setProductionVars(updatedVars);
|
||||
else if (env === 'preview') setPreviewVars(updatedVars);
|
||||
else setDeploymentVars(updatedVars);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} className="flex items-center mb-2">
|
||||
<div className="flex-1 flex items-center justify-between px-3 py-2 rounded-md bg-gray-900 border border-gray-700 mr-2">
|
||||
<span>{variable.key}</span>
|
||||
<span>{variable.value}</span>
|
||||
</div>
|
||||
<div className="flex space-x-1">
|
||||
<button
|
||||
className="p-2 hover:bg-gray-800 rounded-md"
|
||||
onClick={() => editVariable(env, index)}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="p-2 hover:bg-gray-800 rounded-md"
|
||||
onClick={() => removeVariable(env, index)}
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSaving && <LoadingOverlay />}
|
||||
|
||||
<div className="space-y-6 w-full">
|
||||
<div className="rounded-lg border border-gray-800 p-6 bg-black">
|
||||
<h2 className="text-xl font-semibold mb-4">Environment Variables</h2>
|
||||
<p className="text-sm text-gray-400 mb-6">
|
||||
A new deployment is required for your changes to take effect.
|
||||
</p>
|
||||
|
||||
{!isAddingVar ? (
|
||||
<button
|
||||
className="flex items-center space-x-2 px-4 py-2 rounded-md border border-gray-700 bg-gray-900 hover:bg-gray-800 transition-colors"
|
||||
onClick={() => setIsAddingVar(true)}
|
||||
>
|
||||
<PlusIcon size={16} />
|
||||
<span>Create new variable</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="space-y-4 mb-6 border border-gray-800 p-4 rounded-md">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Key</label>
|
||||
<input
|
||||
value={newVarKey}
|
||||
onChange={(e) => setNewVarKey(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
||||
placeholder="KEY"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Value</label>
|
||||
<input
|
||||
value={newVarValue}
|
||||
onChange={(e) => setNewVarValue(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
||||
placeholder="Value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Environments</label>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="env-production"
|
||||
checked={envSelection.production}
|
||||
onChange={() => handleEnvSelectionChange('production')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<label htmlFor="env-production">Production</label>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="env-preview"
|
||||
checked={envSelection.preview}
|
||||
onChange={() => handleEnvSelectionChange('preview')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<label htmlFor="env-preview">Preview</label>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="env-development"
|
||||
checked={envSelection.development}
|
||||
onChange={() => handleEnvSelectionChange('development')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<label htmlFor="env-development">Development</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
className="px-4 py-2 rounded-md border border-gray-700 hover:bg-gray-800 transition-colors"
|
||||
onClick={cancelAddVariable}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 transition-colors"
|
||||
onClick={addVariable}
|
||||
disabled={!newVarKey.trim() || !newVarValue.trim()}
|
||||
>
|
||||
Add Variable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
<EnvGroup
|
||||
title="Production"
|
||||
isOpen={productionOpen}
|
||||
onToggle={() => setProductionOpen(!productionOpen)}
|
||||
varCount={productionVars.length}
|
||||
>
|
||||
{productionVars.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{productionVars.map((variable, index) => renderEnvVarRow('production', variable, index))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-400">No variables defined</p>
|
||||
)}
|
||||
</EnvGroup>
|
||||
|
||||
<EnvGroup
|
||||
title="Preview"
|
||||
isOpen={previewOpen}
|
||||
onToggle={() => setPreviewOpen(!previewOpen)}
|
||||
varCount={previewVars.length}
|
||||
>
|
||||
{previewVars.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{previewVars.map((variable, index) => renderEnvVarRow('preview', variable, index))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-400">No variables defined</p>
|
||||
)}
|
||||
</EnvGroup>
|
||||
|
||||
<EnvGroup
|
||||
title="Deployment"
|
||||
isOpen={deploymentOpen}
|
||||
onToggle={() => setDeploymentOpen(!deploymentOpen)}
|
||||
varCount={deploymentVars.length}
|
||||
>
|
||||
{deploymentVars.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{deploymentVars.map((variable, index) => renderEnvVarRow('development', variable, index))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-400">No variables defined</p>
|
||||
)}
|
||||
</EnvGroup>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button
|
||||
className="px-4 py-2 rounded-md bg-gray-800 hover:bg-gray-700 transition-colors"
|
||||
onClick={saveChanges}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { PageWrapper } from "@/components/foundation";
|
||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import EnvVarsPage from "./EnvVarsPage";
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRepoData } from '@/hooks/useRepoData';
|
||||
|
||||
export default function EnvironmentVariablesPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
const provider = params.provider as string;
|
||||
|
||||
// Use the hook to get repo data
|
||||
const { repoData } = useRepoData(id);
|
||||
|
||||
// Handle tab changes by navigating to the correct folder
|
||||
const handleTabChange = (value: string) => {
|
||||
const basePath = `/projects/${provider}/ps/${id}`;
|
||||
|
||||
switch (value) {
|
||||
case 'overview':
|
||||
router.push(basePath);
|
||||
break;
|
||||
case 'deployment':
|
||||
router.push(`${basePath}/dep`);
|
||||
break;
|
||||
case 'settings':
|
||||
router.push(`${basePath}/set`);
|
||||
break;
|
||||
case 'git':
|
||||
router.push(`${basePath}/int`);
|
||||
break;
|
||||
case 'env-vars':
|
||||
router.push(`${basePath}/set/env`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: repoData ? `${repoData.name}` : 'Project Settings',
|
||||
actions: [
|
||||
{
|
||||
label: 'Open repo',
|
||||
href: repoData?.html_url || '#',
|
||||
icon: 'external-link',
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: 'View app',
|
||||
href: repoData ? `https://${repoData.name.toLowerCase()}.example.com` : '#',
|
||||
icon: 'external-link',
|
||||
external: true
|
||||
}
|
||||
]
|
||||
}}
|
||||
layout="bento"
|
||||
className="pb-0"
|
||||
>
|
||||
<div className="md:col-span-3 w-full">
|
||||
{/* Tabs navigation */}
|
||||
<Tabs defaultValue="env-vars" className="w-full" onValueChange={handleTabChange}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="deployment">Deployment</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
<TabsTrigger value="git">Git</TabsTrigger>
|
||||
<TabsTrigger value="env-vars">Environment Variables</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{/* Environment Variables content */}
|
||||
<div className="mt-6">
|
||||
<EnvVarsPage />
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { PageWrapper } from "@/components/foundation";
|
||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import GitPage from "../../../(integrations)/int/GitPage";
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRepoData } from '@/hooks/useRepoData';
|
||||
|
||||
export default function GitIntegrationsPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
const provider = params.provider as string;
|
||||
// Use the hook to get repo data
|
||||
const { repoData } = useRepoData(id);
|
||||
|
||||
// Handle tab changes by navigating to the correct folder
|
||||
const handleTabChange = (value: string) => {
|
||||
const basePath = `/projects/${provider}/ps/${id}`;
|
||||
|
||||
switch (value) {
|
||||
case 'overview':
|
||||
router.push(basePath);
|
||||
break;
|
||||
case 'deployment':
|
||||
router.push(`${basePath}/dep`);
|
||||
break;
|
||||
case 'settings':
|
||||
router.push(`${basePath}/set`);
|
||||
break;
|
||||
case 'git':
|
||||
router.push(`${basePath}/int`);
|
||||
break;
|
||||
case 'env-vars':
|
||||
router.push(`${basePath}/set/env`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: repoData ? `${repoData.name}` : 'Project Settings',
|
||||
actions: [
|
||||
{
|
||||
label: 'Open repo',
|
||||
href: repoData?.html_url || '#',
|
||||
icon: 'external-link',
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: 'View app',
|
||||
href: repoData ? `https://${repoData.name.toLowerCase()}.example.com` : '#',
|
||||
icon: 'external-link',
|
||||
external: true
|
||||
}
|
||||
]
|
||||
}}
|
||||
layout="bento"
|
||||
className="pb-0"
|
||||
>
|
||||
<div className="md:col-span-3 w-full">
|
||||
{/* Tabs navigation */}
|
||||
<Tabs defaultValue="git" className="w-full" onValueChange={handleTabChange}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="deployment">Deployment</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
<TabsTrigger value="git">Git</TabsTrigger>
|
||||
<TabsTrigger value="env-vars">Environment Variables</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{/* Git content */}
|
||||
<div className="mt-6">
|
||||
<GitPage />
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { Clipboard } from "lucide-react";
|
||||
import { Dropdown } from "@/components/core/dropdown";
|
||||
import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay";
|
||||
import { useRepoData } from "@/hooks/useRepoData";
|
||||
|
||||
// Create a simple modal component
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
}
|
||||
|
||||
function Modal({ isOpen, onClose, title, children, footer }: ModalProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-lg w-full max-w-md p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium">{title}</h3>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-white">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="mb-6">{children}</div>
|
||||
{footer && <div className="flex justify-end">{footer}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProjectSettingsPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params?.id ? String(params.id) : '';
|
||||
|
||||
// Use the hook to get repo data
|
||||
const { repoData, isLoading } = useRepoData(id);
|
||||
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const [projectDescription, setProjectDescription] = useState("");
|
||||
const [projectId, setProjectId] = useState("");
|
||||
const [selectedAccount, setSelectedAccount] = useState("");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isTransferring, setIsTransferring] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
// Update form values when project data is loaded
|
||||
useEffect(() => {
|
||||
if (repoData) {
|
||||
setProjectName(repoData.name || "");
|
||||
setProjectDescription(repoData.description || "");
|
||||
setProjectId(repoData.id?.toString() || "");
|
||||
}
|
||||
}, [repoData]);
|
||||
|
||||
const accountOptions = [
|
||||
{ label: "Personal Account", value: "account1" },
|
||||
{ label: "Team Account", value: "account2" }
|
||||
];
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setIsSaving(true);
|
||||
console.log("Saving project info:", { projectName, projectDescription });
|
||||
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Show success notification - in a real app you'd use a toast library
|
||||
console.log("Project updated successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to save project info:", error);
|
||||
// Show error notification
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTransfer = async () => {
|
||||
try {
|
||||
setIsTransferring(true);
|
||||
// Transfer project to selected account
|
||||
console.log("Transferring project to:", selectedAccount);
|
||||
// Implement API call to transfer project
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
|
||||
|
||||
// After successful transfer, navigate back to projects list
|
||||
router.push("/dashboard/projects");
|
||||
} catch (error) {
|
||||
console.error("Failed to transfer project:", error);
|
||||
// Show error notification
|
||||
} finally {
|
||||
setIsTransferring(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
// Delete project
|
||||
console.log("Deleting project");
|
||||
// Implement API call to delete project
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
|
||||
|
||||
// After successful deletion, navigate back to projects list
|
||||
router.push("/dashboard/projects");
|
||||
} catch (error) {
|
||||
console.error("Failed to delete project:", error);
|
||||
// Show error notification
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setIsDeleteModalOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
};
|
||||
|
||||
const DeleteModalFooter = (
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-md text-white"
|
||||
onClick={() => setIsDeleteModalOpen(false)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 rounded-md text-white"
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? "Deleting..." : "Delete"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingOverlay />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isSaving || isTransferring || isDeleting) && <LoadingOverlay />}
|
||||
|
||||
<div className="space-y-8 w-full">
|
||||
<div className="rounded-lg border border-gray-800 p-6 bg-black">
|
||||
<h2 className="text-xl font-semibold mb-4">Project Info</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="appName" className="block text-sm font-medium mb-1">
|
||||
App name
|
||||
</label>
|
||||
<input
|
||||
id="appName"
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="description" className="block text-sm font-medium mb-1">
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
id="description"
|
||||
value={projectDescription}
|
||||
onChange={(e) => setProjectDescription(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="projectId" className="block text-sm font-medium mb-1">
|
||||
Project ID
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="projectId"
|
||||
value={projectId}
|
||||
readOnly
|
||||
className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white pr-10"
|
||||
/>
|
||||
<button
|
||||
className="absolute right-2 top-1/2 transform -translate-y-1/2"
|
||||
onClick={() => copyToClipboard(projectId)}
|
||||
aria-label="Copy project ID"
|
||||
>
|
||||
<Clipboard className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="px-4 py-2 border border-gray-600 rounded-md hover:bg-gray-800 transition-colors mt-2"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? "Saving..." : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-800 p-6 bg-black">
|
||||
<h2 className="text-xl font-semibold mb-4">Transfer Project</h2>
|
||||
|
||||
<div>
|
||||
<label htmlFor="account" className="block text-sm font-medium mb-1">
|
||||
Select account
|
||||
</label>
|
||||
<Dropdown
|
||||
label="Select"
|
||||
options={accountOptions}
|
||||
selectedValue={selectedAccount}
|
||||
onSelect={(value) => setSelectedAccount(value)}
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
Transfer this app to your personal account or a team you are a member of.
|
||||
</p>
|
||||
|
||||
<button
|
||||
className="px-4 py-2 border border-gray-600 rounded-md hover:bg-gray-800 transition-colors mt-4"
|
||||
onClick={handleTransfer}
|
||||
disabled={!selectedAccount || isTransferring}
|
||||
>
|
||||
{isTransferring ? "Transferring..." : "Transfer"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-800 border-red-900 p-6 bg-black">
|
||||
<h2 className="text-xl font-semibold mb-4 text-red-500">Delete Project</h2>
|
||||
|
||||
<p className="text-sm text-gray-400 mb-4">
|
||||
The project will be permanently deleted, including its deployments and domains. This action is
|
||||
irreversible and cannot be undone.
|
||||
</p>
|
||||
|
||||
<button
|
||||
className="px-4 py-2 bg-red-700 hover:bg-red-800 rounded-md text-white transition-colors"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
>
|
||||
Delete project
|
||||
</button>
|
||||
|
||||
<Modal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => !isDeleting && setIsDeleteModalOpen(false)}
|
||||
title="Are you absolutely sure?"
|
||||
footer={DeleteModalFooter}
|
||||
>
|
||||
<p className="text-gray-300">
|
||||
This action cannot be undone. This will permanently delete the project
|
||||
and all associated deployments and domains.
|
||||
</p>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { PageWrapper } from "@/components/foundation";
|
||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import ProjectSettingsPage from "./ProjectSettingsPage";
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRepoData } from '@/hooks/useRepoData';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
// Safely unwrap params
|
||||
const id = params?.id ? String(params.id) : '';
|
||||
const provider = params?.provider ? String(params.provider) : '';
|
||||
|
||||
// Use the hook to get repo data
|
||||
const { repoData } = useRepoData(id);
|
||||
|
||||
// Handle tab changes by navigating to the correct folder
|
||||
const handleTabChange = (value: string) => {
|
||||
const basePath = `/projects/${provider}/ps/${id}`;
|
||||
|
||||
switch (value) {
|
||||
case 'overview':
|
||||
router.push(basePath);
|
||||
break;
|
||||
case 'deployment':
|
||||
router.push(`${basePath}/dep`);
|
||||
break;
|
||||
case 'settings':
|
||||
router.push(`${basePath}/set`);
|
||||
break;
|
||||
case 'git':
|
||||
router.push(`${basePath}/int`);
|
||||
break;
|
||||
case 'env-vars':
|
||||
router.push(`${basePath}/set/env`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: repoData ? `${repoData.name}` : 'Project Settings',
|
||||
actions: [
|
||||
{
|
||||
label: 'Open repo',
|
||||
href: repoData?.html_url || '#',
|
||||
icon: 'external-link',
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: 'View app',
|
||||
href: repoData ? `https://${repoData.name.toLowerCase()}.example.com` : '#',
|
||||
icon: 'external-link',
|
||||
external: true
|
||||
}
|
||||
]
|
||||
}}
|
||||
layout="bento"
|
||||
className="pb-0"
|
||||
>
|
||||
<div className="md:col-span-3 w-full">
|
||||
{/* Tabs navigation */}
|
||||
<Tabs defaultValue="settings" className="w-full" onValueChange={handleTabChange}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="deployment">Deployment</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
<TabsTrigger value="git">Git</TabsTrigger>
|
||||
<TabsTrigger value="env-vars">Environment Variables</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{/* Settings content */}
|
||||
<div className="mt-6">
|
||||
<ProjectSettingsPage />
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import { FixedProjectCard } from '@/components/projects/project/ProjectCard/FixedProjectCard'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Shapes } from 'lucide-react'
|
||||
import { useAuth } from '@clerk/nextjs'
|
||||
|
||||
interface Deployment {
|
||||
id: string
|
||||
name: string
|
||||
repositoryId: string
|
||||
status: 'running' | 'complete' | 'failed'
|
||||
url?: string
|
||||
branch: string
|
||||
createdAt: string
|
||||
createdBy: {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const [deployments, setDeployments] = useState<Deployment[]>([])
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const { isLoaded: isAuthLoaded, userId } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchDeployments() {
|
||||
if (!isAuthLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
if (!userId) {
|
||||
setError('Not authenticated');
|
||||
return;
|
||||
}
|
||||
|
||||
// In a real implementation, you would query your GraphQL backend
|
||||
// For now, we'll mock some deployments
|
||||
const mockDeployments: Deployment[] = [
|
||||
{
|
||||
id: 'dep_abc123',
|
||||
name: 'My Project',
|
||||
repositoryId: '123456',
|
||||
status: 'complete',
|
||||
url: 'https://my-project.example.com',
|
||||
branch: 'main',
|
||||
createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: {
|
||||
name: 'John Doe'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'dep_def456',
|
||||
name: 'Another Project',
|
||||
repositoryId: '789012',
|
||||
status: 'running',
|
||||
branch: 'develop',
|
||||
createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: {
|
||||
name: 'Jane Smith'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
setDeployments(mockDeployments);
|
||||
} catch (err) {
|
||||
console.error('Error fetching deployments:', err)
|
||||
setError('Failed to fetch deployments')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchDeployments()
|
||||
}, [isAuthLoaded, userId]);
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: 'Projects',
|
||||
actions: [{ label: 'Create Project', href: '/projects/create' }]
|
||||
}}
|
||||
layout="bento"
|
||||
className="pb-0"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="md:col-span-3 flex justify-center items-center min-h-[600px]">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="md:col-span-3 border border-gray-800 rounded-lg min-h-[600px] flex flex-col items-center justify-center p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<Shapes size={64} className="stroke-current" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2">Error: {error}</h2>
|
||||
<p className="text-gray-400 text-center max-w-md mb-6">
|
||||
There was an error loading your deployments.
|
||||
</p>
|
||||
<Button
|
||||
className="bg-white text-black hover:bg-gray-200"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
) : deployments.length === 0 ? (
|
||||
<div className="md:col-span-3 border border-gray-800 rounded-lg min-h-[600px] flex flex-col items-center justify-center p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<Shapes size={64} className="stroke-current" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2">Deploy your first app</h2>
|
||||
<p className="text-gray-400 text-center max-w-md mb-6">
|
||||
You haven't deployed any projects yet. Start by importing a repository from your GitHub account.
|
||||
</p>
|
||||
<Button
|
||||
className="bg-white text-black hover:bg-gray-200"
|
||||
onClick={() => window.location.href = '/projects/create'}
|
||||
>
|
||||
Create Project
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="md:col-span-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{deployments.map((deployment) => (
|
||||
<FixedProjectCard
|
||||
project={{
|
||||
id: deployment.id,
|
||||
name: deployment.name,
|
||||
deployments: [{
|
||||
applicationDeploymentRecordData: {
|
||||
url: deployment.url
|
||||
},
|
||||
branch: deployment.branch,
|
||||
createdAt: deployment.createdAt,
|
||||
createdBy: deployment.createdBy
|
||||
}]
|
||||
}}
|
||||
key={deployment.id}
|
||||
status={deployment.status === 'complete' ? 'success' :
|
||||
deployment.status === 'running' ? 'pending' : 'error'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode
|
||||
params: {
|
||||
id: string
|
||||
provider: string
|
||||
orgSlug: string
|
||||
}
|
||||
}
|
||||
|
||||
export default function ProjectLayout({ children }: LayoutProps) {
|
||||
return <div className="flex flex-col min-h-0 flex-1">{children}</div>
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<PageWrapper>
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-12 w-12 bg-gray-200 rounded-full" />
|
||||
<div className="h-4 w-48 bg-gray-200 rounded" />
|
||||
<div className="h-4 w-64 bg-gray-200 rounded" />
|
||||
<div className="h-4 w-32 bg-gray-200 rounded" />
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { PageWrapper } from '@/components/foundation';
|
||||
import { getInitials } from '@/utils/getInitials';
|
||||
import { relativeTimeMs } from '@/utils/time';
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback} from '@workspace/ui/components/avatar';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { Activity, Clock, GitBranch, ExternalLink } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRepoData } from '@/hooks/useRepoData';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function ProjectOverviewPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
// Safely unwrap params
|
||||
const id = params?.id ? String(params.id) : '';
|
||||
const provider = params?.provider ? String(params.provider) : '';
|
||||
|
||||
// Use the hook to get repo data
|
||||
const { repoData } = useRepoData(id);
|
||||
|
||||
// Default deployment details
|
||||
const [deploymentUrl, setDeploymentUrl] = useState('');
|
||||
const [deploymentDate, setDeploymentDate] = useState(Date.now() - 60 * 60 * 1000); // 1 hour ago
|
||||
const [deployedBy, setDeployedBy] = useState('');
|
||||
const [projectName, setProjectName] = useState('');
|
||||
const [branch, setBranch] = useState('main');
|
||||
|
||||
// Update details when repo data is loaded
|
||||
useEffect(() => {
|
||||
if (repoData) {
|
||||
setProjectName(repoData.name);
|
||||
setBranch(repoData.default_branch || 'main');
|
||||
setDeployedBy(repoData.owner?.login || 'username');
|
||||
// Create a deployment URL based on the repo name
|
||||
setDeploymentUrl(`https://${repoData.name.toLowerCase()}.example.com`);
|
||||
}
|
||||
}, [repoData]);
|
||||
|
||||
// Auction data
|
||||
const auctionId = 'laconic1sdfjwei4jfkasifgjiai45ioasjf5jjjafij355';
|
||||
|
||||
// Activities data
|
||||
const activities = [
|
||||
{
|
||||
username: deployedBy || 'username',
|
||||
branch: branch,
|
||||
action: 'deploy: source cargo',
|
||||
time: '5 minutes ago'
|
||||
},
|
||||
{
|
||||
username: deployedBy || 'username',
|
||||
branch: branch,
|
||||
action: 'bump',
|
||||
time: '5 minutes ago'
|
||||
},
|
||||
{
|
||||
username: deployedBy || 'username',
|
||||
branch: branch,
|
||||
action: 'version: update version',
|
||||
time: '5 minutes ago'
|
||||
},
|
||||
{
|
||||
username: deployedBy || 'username',
|
||||
branch: branch,
|
||||
action: 'build: updates',
|
||||
time: '5 minutes ago'
|
||||
}
|
||||
];
|
||||
|
||||
// Handle tab changes by navigating to the correct folder
|
||||
const handleTabChange = (value: string) => {
|
||||
const basePath = `/projects/${provider}/ps/${id}`;
|
||||
|
||||
switch (value) {
|
||||
case 'overview':
|
||||
router.push(basePath);
|
||||
break;
|
||||
case 'deployment':
|
||||
router.push(`${basePath}/dep`);
|
||||
break;
|
||||
case 'settings':
|
||||
router.push(`${basePath}/set`);
|
||||
break;
|
||||
case 'git':
|
||||
router.push(`${basePath}/int`);
|
||||
break;
|
||||
case 'env-vars':
|
||||
router.push(`${basePath}/set/env`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: projectName || 'Project Overview',
|
||||
actions: [
|
||||
{
|
||||
label: 'Open repo',
|
||||
href: repoData?.html_url || '#',
|
||||
icon: 'external-link',
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: 'View app',
|
||||
href: deploymentUrl || '#',
|
||||
icon: 'external-link',
|
||||
external: true
|
||||
}
|
||||
]
|
||||
}}
|
||||
layout="bento" // Use bento layout to override max width
|
||||
className="pb-0"
|
||||
>
|
||||
<div className="md:col-span-3 w-full"> {/* Take full width in bento grid */}
|
||||
{/* Tabs navigation */}
|
||||
<Tabs defaultValue="overview" className="w-full" onValueChange={handleTabChange}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="deployment">Deployment</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
<TabsTrigger value="git">Git</TabsTrigger>
|
||||
<TabsTrigger value="env-vars">Environment Variables</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="pt-6">
|
||||
{/* Main content card (containing project info and auction details) */}
|
||||
<div className="bg-background border border-border rounded-lg overflow-hidden mb-8">
|
||||
{/* Project info section */}
|
||||
<div className="p-6">
|
||||
<div className="flex items-center">
|
||||
<Avatar className="h-10 w-10 mr-4 bg-blue-600">
|
||||
<AvatarFallback>{getInitials(projectName || '')}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">{projectName}</h2>
|
||||
<p className="text-muted-foreground">
|
||||
{deploymentUrl.replace(/^https?:\/\//, '')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
|
||||
<div>
|
||||
<div className="flex items-center mb-2">
|
||||
<GitBranch className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||
<span className="text-muted-foreground text-sm">Source</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<GitBranch className="h-4 w-4 mr-2" />
|
||||
<span>{branch}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center mb-2">
|
||||
<ExternalLink className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||
<span className="text-muted-foreground text-sm">Deployment URL</span>
|
||||
</div>
|
||||
<Link
|
||||
href={deploymentUrl}
|
||||
className="text-primary hover:underline flex items-center"
|
||||
target="_blank"
|
||||
>
|
||||
{deploymentUrl}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center mb-2">
|
||||
<Clock className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||
<span className="text-muted-foreground text-sm">Deployment date</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">
|
||||
{relativeTimeMs(deploymentDate)}
|
||||
</span>
|
||||
<span className="mr-2">by</span>
|
||||
<Avatar className="h-5 w-5 mr-2">
|
||||
<AvatarFallback>{getInitials(deployedBy)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span>{deployedBy}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider between project info and auction details */}
|
||||
<div className="border-t border-border my-6"></div>
|
||||
|
||||
{/* Auction Details section */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-6">Auction Details</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 className="text-sm text-muted-foreground mb-1">Auction ID</h4>
|
||||
<p className="text-sm font-medium break-all">{auctionId}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm text-muted-foreground mb-1">Auction Status</h4>
|
||||
<div className="inline-block px-2 py-0.5 bg-green-700/20 text-green-400 text-xs font-medium rounded">
|
||||
COMPLETED
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
|
||||
<div>
|
||||
<h4 className="text-sm text-muted-foreground mb-1">Deployer LRNs</h4>
|
||||
<p className="text-sm font-medium break-all">{auctionId}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm text-muted-foreground mb-1">Deployer Funds Status</h4>
|
||||
<div className="inline-block px-2 py-0.5 bg-blue-700/20 text-blue-400 text-xs font-medium rounded">
|
||||
RELEASED
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button variant="outline" size="sm">View details</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activity section - not in a card */}
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg font-medium mb-6 flex items-center">
|
||||
<Activity className="mr-2 h-4 w-4" />
|
||||
Activity
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
{activities.map((activity, index) => (
|
||||
<div key={index} className="flex items-start">
|
||||
<div className="text-muted-foreground mr-2">•</div>
|
||||
<div className="flex-1">
|
||||
<span className="text-sm mr-2">{activity.username}</span>
|
||||
<GitBranch className="inline h-3 w-3 text-muted-foreground mx-1" />
|
||||
<span className="text-sm text-muted-foreground mr-2">{activity.branch}</span>
|
||||
<span className="text-sm text-muted-foreground">{activity.action}</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{activity.time}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* These content sections won't be shown - we'll navigate to respective pages instead */}
|
||||
<TabsContent value="deployment"></TabsContent>
|
||||
<TabsContent value="settings"></TabsContent>
|
||||
<TabsContent value="git"></TabsContent>
|
||||
<TabsContent value="env-vars"></TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function ClientError({
|
||||
error,
|
||||
reset
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}) {
|
||||
useEffect(() => {
|
||||
// Log the error to an error reporting service
|
||||
console.error(error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<div className="p-8 text-center">
|
||||
<h2 className="text-xl font-bold mb-4">Something went wrong!</h2>
|
||||
<p className="mb-4 text-gray-600">{error.message}</p>
|
||||
<Button
|
||||
onClick={reset}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Try again
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<div className="animate-pulse text-center">
|
||||
<div className="h-6 w-24 bg-gray-200 rounded mb-4 mx-auto" />
|
||||
|
||||
<div className="h-32 w-full max-w-md bg-gray-200 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
'use client'
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import CheckBalanceIframe from '@/components/iframe/check-balance-iframe/CheckBalanceIframe'
|
||||
import type { Project } from '@octokit/webhooks-types'
|
||||
import { FixedProjectCard } from '@/components/projects/project/ProjectCard/FixedProjectCard'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Shapes } from 'lucide-react'
|
||||
import { useAuth, useUser } from '@clerk/nextjs'
|
||||
import { useRepoData } from '@/hooks/useRepoData'
|
||||
|
||||
interface ProjectData {
|
||||
id: string
|
||||
name: string
|
||||
icon?: string
|
||||
deployments: any[]
|
||||
// Additional fields from GitHub repo
|
||||
full_name?: string
|
||||
html_url?: string
|
||||
updated_at?: string
|
||||
default_branch?: string
|
||||
}
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const [, setIsBalanceSufficient] = useState<boolean>()
|
||||
const [projects, setProjects] = useState<Project[]>([])
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const { isLoaded: isAuthLoaded, userId } = useAuth()
|
||||
const { isLoaded: isUserLoaded, user } = useUser()
|
||||
|
||||
// Use the hook to fetch all repos (with an empty ID to get all)
|
||||
const { repoData: allRepos, isLoading: reposLoading, error: reposError } = useRepoData('');
|
||||
|
||||
const handleConnectGitHub = () => {
|
||||
window.open('https://accounts.clerk.dev/user', '_blank');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Process repos data when it's loaded
|
||||
if (!reposLoading && allRepos) {
|
||||
// Transform GitHub repos to match ProjectData interface
|
||||
const projectData: ProjectData[] = allRepos.map((repo: any) => ({
|
||||
id: repo.id.toString(),
|
||||
name: repo.name,
|
||||
full_name: repo.full_name,
|
||||
// Create a deployment object that matches your existing structure
|
||||
deployments: [
|
||||
{
|
||||
applicationDeploymentRecordData: {
|
||||
url: repo.html_url
|
||||
},
|
||||
branch: repo.default_branch,
|
||||
createdAt: repo.updated_at,
|
||||
createdBy: {
|
||||
name: repo.owner?.login || 'Unknown'
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
setProjects(projectData);
|
||||
setIsLoading(false);
|
||||
} else if (!reposLoading && reposError) {
|
||||
setError(reposError);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [allRepos, reposLoading, reposError]);
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: 'Projects',
|
||||
actions: [{ label: 'Create Project', href: '/projects/create' }]
|
||||
}}
|
||||
layout="bento"
|
||||
className="pb-0"
|
||||
>
|
||||
{isLoading ? (
|
||||
// Full width loading spinner in bento layout
|
||||
<div className="md:col-span-3 flex justify-center items-center min-h-[600px]">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
) : error ? (
|
||||
// Full width error state in bento layout
|
||||
<div className="md:col-span-3 border border-gray-800 rounded-lg min-h-[600px] flex flex-col items-center justify-center p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<Shapes size={64} className="stroke-current" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2">Error: {error}</h2>
|
||||
<p className="text-gray-400 text-center max-w-md mb-6">
|
||||
Please connect your GitHub account to see your repositories.
|
||||
</p>
|
||||
<Button
|
||||
className="bg-white text-black hover:bg-gray-200 flex items-center"
|
||||
onClick={handleConnectGitHub}
|
||||
>
|
||||
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||
</svg>
|
||||
Connect to GitHub
|
||||
</Button>
|
||||
</div>
|
||||
) : projects.length === 0 ? (
|
||||
// Full width empty state in bento layout
|
||||
<div className="md:col-span-3 border border-gray-800 rounded-lg min-h-[600px] flex flex-col items-center justify-center p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<Shapes size={64} className="stroke-current" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2">Deploy your first app</h2>
|
||||
<p className="text-gray-400 text-center max-w-md mb-6">
|
||||
Once connected, you can import a repository from your account or start with one of our templates.
|
||||
</p>
|
||||
<Button
|
||||
className="bg-white text-black hover:bg-gray-200 flex items-center"
|
||||
onClick={handleConnectGitHub}
|
||||
>
|
||||
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||
</svg>
|
||||
Connect to GitHub
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
// Custom grid that spans the entire bento layout
|
||||
<div className="md:col-span-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{projects.map((project) => (
|
||||
<FixedProjectCard
|
||||
project={project as any}
|
||||
key={project.id}
|
||||
status={project.deployments[0]?.branch ? 'success' : 'pending'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Wrap in try/catch to prevent breaking if there are issues */}
|
||||
{(() => {
|
||||
try {
|
||||
return (
|
||||
<CheckBalanceIframe
|
||||
onBalanceChange={setIsBalanceSufficient}
|
||||
isPollingEnabled={false}
|
||||
amount="1"
|
||||
/>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to render CheckBalanceIframe:', error);
|
||||
return null;
|
||||
}
|
||||
})()}
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
'use client'
|
||||
|
||||
import { PageWrapper } from '@/components/foundation/page-wrapper'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@workspace/ui/components/card'
|
||||
/**
|
||||
* BuyPrepaidService component allows users to buy prepaid services.
|
||||
* It checks if the user's balance is sufficient and redirects them to the home page if it is.
|
||||
*
|
||||
* @returns {JSX.Element} A JSX element that renders the buy prepaid service page.
|
||||
*/
|
||||
const BuyPrepaidService = () => {
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: 'Buy Prepaid Service',
|
||||
subtitle: 'Choose a plan that fits your deployment needs'
|
||||
}}
|
||||
>
|
||||
<div className=" mx-auto px-4 py-16 md:py-18 w-full">
|
||||
<div className="mx-auto w-full min-w-fit">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="font-normal text-gray-400 tracking-tight sm:text-2xl text-foreground">
|
||||
Laconic
|
||||
</h2>
|
||||
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl text-foreground">
|
||||
Webapp Deployment Plans
|
||||
</h1>
|
||||
<p className="mt-4 text-muted-foreground">
|
||||
Choose the perfect deployment plan for your needs. Scale your
|
||||
applications with confidence.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3 w-full">
|
||||
{/* Basic Plan */}
|
||||
<Card className="border border-gray-200">
|
||||
<CardHeader>
|
||||
<CardTitle>Basic</CardTitle>
|
||||
<CardDescription>
|
||||
A simple deployment option for small projects
|
||||
</CardDescription>
|
||||
<div className="mt-4">
|
||||
<span className="text-3xl font-bold">$5.00</span>
|
||||
<span className="text-muted-foreground ml-1">/month</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
1 monthly webapp deployment
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full" variant="outline" asChild>
|
||||
<a
|
||||
href="https://store.laconic.com/products/1-webapp-deployment"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Purchase plan
|
||||
</a>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Standard Plan */}
|
||||
<Card className="border-2 border-primary relative">
|
||||
<div className="absolute -top-3 left-0 right-0 mx-auto w-fit rounded-full bg-primary px-3 py-1 text-xs font-medium text-primary-foreground">
|
||||
Most popular
|
||||
</div>
|
||||
<CardHeader>
|
||||
<CardTitle>Standard</CardTitle>
|
||||
<CardDescription>
|
||||
Perfect for growing projects and businesses
|
||||
</CardDescription>
|
||||
<div className="mt-4">
|
||||
<span className="text-3xl font-bold">$50.00</span>
|
||||
<span className="text-muted-foreground ml-1">/month</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
10 monthly webapp deployments
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full" asChild>
|
||||
<a
|
||||
href="https://store.laconic.com/products/10-webapp-deployments"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Purchase plan
|
||||
</a>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Premium Plan */}
|
||||
<Card className="border border-gray-200">
|
||||
<CardHeader>
|
||||
<CardTitle>Premium</CardTitle>
|
||||
<CardDescription>
|
||||
For enterprises with high-volume needs
|
||||
</CardDescription>
|
||||
<div className="mt-4">
|
||||
<span className="text-3xl font-bold">$500.00</span>
|
||||
<span className="text-muted-foreground ml-1">/month</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
100 monthly webapp deployments
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full" variant="outline" asChild>
|
||||
<a
|
||||
href="https://store.laconic.com/products/100-webapp-deployments"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Purchase plan
|
||||
</a>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default BuyPrepaidService
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Metadata } from 'next'
|
||||
import BuyPrepaidService from './BuyServices'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Buy Prepaid Service',
|
||||
description: 'Buy prepaid service page description'
|
||||
}
|
||||
|
||||
const Page = () => {
|
||||
return <BuyPrepaidService />
|
||||
}
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,19 @@
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Store Page',
|
||||
description: 'Store page description'
|
||||
}
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<PageWrapper header={{ title: 'Store' }}>
|
||||
<div>
|
||||
<h1>Hello from store</h1>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
|
||||
import {
|
||||
Bell,
|
||||
ChevronRight,
|
||||
CreditCard,
|
||||
Globe,
|
||||
HelpCircle,
|
||||
Lock,
|
||||
Shield,
|
||||
User
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { ComingSoonOverlay } from '@/components/foundation'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Separator } from '@workspace/ui/components/separator'
|
||||
import { useRouter } from 'next/navigation'
|
||||
/**
|
||||
* Settings category item component
|
||||
* Renders a single settings category with an icon and label
|
||||
*/
|
||||
interface SettingsCategoryProps {
|
||||
/** The icon to display for this category */
|
||||
icon: React.ReactNode
|
||||
/** The label text for this category */
|
||||
label: string
|
||||
/** Whether this category is currently active */
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
function SettingsCategory({
|
||||
icon,
|
||||
label,
|
||||
isActive = false
|
||||
}: SettingsCategoryProps) {
|
||||
return (
|
||||
<Link
|
||||
href="#"
|
||||
className={`flex items-center gap-3 px-4 py-2 rounded-md transition-colors ${
|
||||
isActive ? 'bg-muted font-medium' : 'hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
{isActive && <ChevronRight className="ml-auto h-4 w-4" />}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings page component
|
||||
* Displays a simple settings interface with categories and placeholder content
|
||||
*/
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter()
|
||||
const [activeCategory] = useState('profile')
|
||||
const [formState, setFormState] = useState({
|
||||
darkTheme: false,
|
||||
autoDetectTimezone: true,
|
||||
allowAnalytics: true
|
||||
})
|
||||
|
||||
/**
|
||||
* Handle checkbox changes
|
||||
*/
|
||||
const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { id, checked } = e.target
|
||||
setFormState((prev) => ({
|
||||
...prev,
|
||||
[id]: checked
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<ComingSoonOverlay
|
||||
message="Settings configuration will be available in the next release."
|
||||
routerAction={() => router.back()}
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<header className="border-b">
|
||||
<div className="container py-4">
|
||||
<div className="flex items-center">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<Globe className="h-6 w-6" />
|
||||
<span className="font-bold">Laconic Deploy</span>
|
||||
</Link>
|
||||
<h1 className="text-xl font-semibold ml-8">Settings</h1>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container py-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-[250px_1fr] gap-8">
|
||||
{/* Sidebar */}
|
||||
<nav className="space-y-1">
|
||||
<SettingsCategory
|
||||
icon={<User className="h-5 w-5" />}
|
||||
label="Profile"
|
||||
isActive={activeCategory === 'profile'}
|
||||
/>
|
||||
<SettingsCategory
|
||||
icon={<Bell className="h-5 w-5" />}
|
||||
label="Notifications"
|
||||
/>
|
||||
<SettingsCategory
|
||||
icon={<Lock className="h-5 w-5" />}
|
||||
label="Password & Security"
|
||||
/>
|
||||
<SettingsCategory
|
||||
icon={<Shield className="h-5 w-5" />}
|
||||
label="Access Control"
|
||||
/>
|
||||
<SettingsCategory
|
||||
icon={<CreditCard className="h-5 w-5" />}
|
||||
label="Billing"
|
||||
/>
|
||||
<SettingsCategory
|
||||
icon={<HelpCircle className="h-5 w-5" />}
|
||||
label="Help & Support"
|
||||
/>
|
||||
</nav>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">
|
||||
Profile Settings
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Manage your account information and preferences.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-2">
|
||||
<h3 className="text-lg font-medium">Personal Information</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Update your personal details and how we contact you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="name" className="text-sm font-medium">
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="John Doe"
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="text-sm font-medium">
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="john@example.com"
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button>Save Changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-2">
|
||||
<h3 className="text-lg font-medium">Preferences</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Customize your experience with Laconic Deploy.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
id="darkTheme"
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={formState.darkTheme}
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
<label htmlFor="darkTheme" className="text-sm font-medium">
|
||||
Use dark theme
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
id="autoDetectTimezone"
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={formState.autoDetectTimezone}
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
<label
|
||||
htmlFor="autoDetectTimezone"
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Automatically detect timezone
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
id="allowAnalytics"
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={formState.allowAnalytics}
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
<label
|
||||
htmlFor="allowAnalytics"
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Allow anonymous usage data collection
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button>Save Preferences</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className="border-t mt-auto">
|
||||
<div className="container py-6">
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} Laconic Deploy. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import SupportPlaceholder from './SupportPlaceholder'
|
||||
|
||||
export default function SupportPage() {
|
||||
return (
|
||||
<PageWrapper header={{ title: 'Support' }}>
|
||||
<SupportPlaceholder />
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Wallet Page',
|
||||
description: 'Wallet page description'
|
||||
}
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<PageWrapper header={{ title: 'Wallet' }}>
|
||||
<div>
|
||||
<h1>Hello from wallet</h1>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,11 @@
|
||||
import type React from 'react'
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Layout: React.FC<LayoutProps> = async ({ children }) => {
|
||||
return <main>{children}</main>
|
||||
}
|
||||
|
||||
export default Layout
|
||||
@@ -0,0 +1,36 @@
|
||||
// app/actions/github.ts
|
||||
'use server'
|
||||
|
||||
import { auth, currentUser } from '@clerk/nextjs/server'
|
||||
import { Octokit } from '@octokit/rest'
|
||||
import type { Organization } from '@octokit/webhooks-types'
|
||||
|
||||
export async function getGitHubOrgs() {
|
||||
const { userId } = await auth()
|
||||
|
||||
if (!userId) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
const user = await currentUser()
|
||||
const githubAccount = user?.externalAccounts.find(
|
||||
(account) => account.provider === 'github'
|
||||
)
|
||||
|
||||
const token =
|
||||
githubAccount?.provider === 'github' ? githubAccount.externalId : null
|
||||
|
||||
if (!token) {
|
||||
throw new Error('GitHub not connected')
|
||||
}
|
||||
|
||||
const octokit = new Octokit({ auth: token })
|
||||
const { data } = await octokit.rest.orgs.listForAuthenticatedUser()
|
||||
|
||||
return data.map((org: Organization) => ({
|
||||
id: org.id,
|
||||
name: org.login,
|
||||
login: org.login,
|
||||
avatarUrl: org.avatar_url
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// nextjs route handler, checks import { NextResponse } from 'next/server'
|
||||
import { auth } from '@clerk/nextjs/server'
|
||||
import { clerkClient } from '@clerk/nextjs/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET() {
|
||||
await auth.protect()
|
||||
|
||||
const clerk = await clerkClient()
|
||||
const authenticated = await auth()
|
||||
|
||||
// if (!userId) {
|
||||
// return new Response('Unauthorized', { status: 401 })
|
||||
// }
|
||||
|
||||
// if (!userId) {
|
||||
// return new Response('Unauthorized', { status: 401 })
|
||||
// }
|
||||
|
||||
const userId = authenticated.userId
|
||||
|
||||
if (!userId) {
|
||||
return new Response(JSON.stringify({ error: 'Error: No signed in user' }), {
|
||||
status: 401
|
||||
})
|
||||
}
|
||||
|
||||
console.log('Frome the api')
|
||||
// try {
|
||||
// let res
|
||||
// if (userId) {
|
||||
const res = await clerk.users.getUserOauthAccessToken(userId, 'github')
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error(error)
|
||||
// }
|
||||
// const github = await clerk.getUserOAuthAccessToken('oauth_github')
|
||||
|
||||
// const { userId } = await auth()
|
||||
// if (!userId) {
|
||||
// return new Response('Unauthorized', { status: 401 })
|
||||
// }
|
||||
|
||||
// const token = await getToken({ template: 'oauth_github' })
|
||||
|
||||
// Fetch data from Supabase and return it.
|
||||
// const data = { token: token }
|
||||
// console.log(data)
|
||||
//
|
||||
const data = res.data[0]?.token
|
||||
return NextResponse.json({ res: data })
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { createHmac, timingSafeEqual } from 'node:crypto'
|
||||
import type { WebhookEventName } from '@octokit/webhooks-types'
|
||||
|
||||
// Add GET handler for GitHub webhook verification
|
||||
|
||||
export async function GET() {
|
||||
return new Response('Ready to receive webhooks', { status: 200 })
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const payload = await request.text()
|
||||
const signature256 = request.headers.get('x-hub-signature-256')
|
||||
const signature1 = request.headers.get('x-hub-signature')
|
||||
const eventType = request.headers.get('x-github-event')
|
||||
const event = eventType as WebhookEventName
|
||||
|
||||
console.log('Received webhook:', {
|
||||
event,
|
||||
signatures: { signature256, signature1 }
|
||||
})
|
||||
|
||||
// Always use SHA256 if available
|
||||
if (!signature256) {
|
||||
return new Response('SHA256 signature required', { status: 401 })
|
||||
}
|
||||
|
||||
const secret = process.env.GITHUB_WEBHOOK_SECRET
|
||||
if (!secret) {
|
||||
return new Response('Webhook secret not configured', { status: 500 })
|
||||
}
|
||||
|
||||
// Calculate expected SHA256 hash
|
||||
const expectedHash = createHmac('sha256', secret)
|
||||
.update(payload)
|
||||
.digest('hex')
|
||||
|
||||
const providedHash = signature256.replace('sha256=', '')
|
||||
|
||||
console.log('Verifying signature:', {
|
||||
provided: providedHash,
|
||||
expected: expectedHash,
|
||||
match: providedHash === expectedHash
|
||||
})
|
||||
|
||||
const signatureMatches = timingSafeEqual(
|
||||
Buffer.from(providedHash),
|
||||
Buffer.from(expectedHash)
|
||||
)
|
||||
|
||||
if (!signatureMatches) {
|
||||
return new Response('Invalid signature', { status: 401 })
|
||||
}
|
||||
|
||||
const data = JSON.parse(payload)
|
||||
console.log('Processing webhook:', { event, data })
|
||||
|
||||
return new Response('OK', { status: 200 })
|
||||
} catch (error: unknown) {
|
||||
console.error('Webhook error:', error)
|
||||
return new Response(
|
||||
error instanceof Error ? error.message : 'Unknown error',
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,38 @@
|
||||
import { Providers } from '@/components/providers'
|
||||
import { ClerkProvider } from '@clerk/nextjs'
|
||||
import '@workspace/ui/globals.css'
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import { CheckBalanceWrapper } from '@/components/iframe/check-balance-iframe/CheckBalanceWrapper'
|
||||
|
||||
// Add root metadata with template pattern
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: '%s | Laconic Deploy',
|
||||
default: 'Laconic Deploy - Cloud Deployment Platform'
|
||||
},
|
||||
description: 'Deploy your applications with Laconic',
|
||||
metadataBase: new URL('https://deploy-laconic.qwrk.app'),
|
||||
icons: {
|
||||
icon: '/favicon.ico'
|
||||
}
|
||||
}
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export default function RootLayout({
|
||||
children
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<ClerkProvider signInFallbackRedirectUrl="/home">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${inter.className} `} suppressHydrationWarning>
|
||||
<main>
|
||||
<Providers>{children}</Providers>
|
||||
<CheckBalanceWrapper />
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
</ClerkProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { LoadingOverlay } from '@/components/loading/loading-overlay'
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingOverlay isLoading={true} />
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Shapes } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useAuth, useUser } from '@clerk/nextjs'
|
||||
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
|
||||
|
||||
export default function HomePage() {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const { isLoaded: isAuthLoaded, userId } = useAuth()
|
||||
const { isLoaded: isUserLoaded } = useUser()
|
||||
|
||||
useEffect(() => {
|
||||
// Check authentication status
|
||||
if (isAuthLoaded && isUserLoaded) {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [isAuthLoaded, isUserLoaded])
|
||||
|
||||
const handleConnectWallet = () => {
|
||||
// Handle wallet connection
|
||||
console.log('Connect wallet clicked')
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingOverlay isLoading={true} />
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: 'Welcome to Laconic Deploy',
|
||||
actions: userId ? [{ label: 'Create Project', href: '/projects/create' }] : []
|
||||
}}
|
||||
layout="bento"
|
||||
className="pb-0"
|
||||
>
|
||||
<div className="md:col-span-3 min-h-[80vh] flex flex-col items-center justify-center">
|
||||
{userId ? (
|
||||
// User is authenticated
|
||||
<div className="flex flex-col items-center text-center max-w-2xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<Shapes size={80} className="stroke-current" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-6">Welcome to Laconic Deploy</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8">
|
||||
Deploy your applications quickly and securely on the decentralized web.
|
||||
</p>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/projects">View My Projects</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link href="/documentation">Documentation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// User is not authenticated
|
||||
<div className="flex flex-col items-center text-center max-w-2xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<Shapes size={80} className="stroke-current" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-6">Decentralized Web Deployment</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8">
|
||||
Deploy your applications securely and efficiently with Laconic's web3 deployment platform.
|
||||
Connect your wallet to get started.
|
||||
</p>
|
||||
|
||||
<Button onClick={handleConnectWallet} size="lg" className="mb-4">
|
||||
Connect Wallet
|
||||
</Button>
|
||||
|
||||
<p className="text-sm text-muted-foreground mt-4">
|
||||
Already have an account? <Link href="/sign-in" className="text-primary hover:underline">Sign in</Link>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Removed the CheckBalanceIframe component that was causing errors */}
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import { SignIn } from '@clerk/nextjs'
|
||||
import { dark } from '@clerk/themes'
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<PageWrapper header={{ title: 'Sign In' }}>
|
||||
<div className="flex justify-center items-center min-h-[calc(100vh-200px)]">
|
||||
<SignIn
|
||||
appearance={{
|
||||
baseTheme: dark,
|
||||
elements: {
|
||||
formButtonPrimary: 'bg-primary hover:bg-primary/90',
|
||||
card: 'shadow-none',
|
||||
rootBox: 'font-inter',
|
||||
formField: 'font-inter',
|
||||
footerAction: 'font-inter',
|
||||
header: 'font-inter',
|
||||
headerTitle: 'font-inter text-2xl font-semibold',
|
||||
headerSubtitle: 'font-inter text-muted-foreground'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* The Laconic logo mark component.
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* // Basic usage
|
||||
* <LaconicMark />
|
||||
*
|
||||
* // With custom color
|
||||
* <LaconicMark className="text-blue-500" />
|
||||
*
|
||||
* // With custom size
|
||||
* <LaconicMark className="w-12 h-12" />
|
||||
*/
|
||||
export function LaconicMark({
|
||||
/**
|
||||
* Optional className for styling the SVG
|
||||
* Use this to customize the size, color, and other SVG properties
|
||||
*/
|
||||
className = ''
|
||||
}: {
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width="80"
|
||||
height="81"
|
||||
viewBox="0 0 80 81"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
aria-label="Laconic logo"
|
||||
>
|
||||
<title>Laconic logo</title>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M23.4359 41.7985C29.255 35.9796 32.8569 27.9437 32.8552 19.0713C32.8574 17.867 32.7909 16.6761 32.6587 15.5L15 15.5016L15.0005 49.4307C14.9989 53.5435 16.567 57.6575 19.7035 60.7938C22.8402 63.9303 26.9571 65.5003 31.0705 65.4982L65 65.5L64.9987 47.8387C63.8252 47.7095 62.6342 47.6425 61.4272 47.6423C52.5572 47.6435 44.521 51.245 38.7019 57.0638C34.4676 61.192 27.6786 61.1927 23.4965 57.0107C19.3172 52.8312 19.3153 46.0402 23.4359 41.7985ZM61.3372 19.1716C56.4605 14.2952 48.5377 14.2933 43.6595 19.1716C38.7812 24.0496 38.7831 31.9724 43.6595 36.8485C48.5387 41.7275 56.459 41.7267 61.3372 36.8485C66.2155 31.9705 66.2165 24.0504 61.3372 19.1716Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type React from 'react'
|
||||
import type { DropdownProps } from './types'
|
||||
|
||||
/**
|
||||
* A simple dropdown component using the native select element.
|
||||
*
|
||||
* @component
|
||||
* @param {DropdownProps} props - The props for the Dropdown component.
|
||||
* @returns {React.ReactElement} A dropdown element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Dropdown
|
||||
* options={[{ value: '1', label: 'One' }, { value: '2', label: 'Two' }]}
|
||||
* onChange={(option) => console.log(option)}
|
||||
* placeholder="Select an option"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const Dropdown = ({
|
||||
placeholder,
|
||||
options,
|
||||
onChange,
|
||||
value
|
||||
}: DropdownProps) => {
|
||||
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedOption = options.find(
|
||||
(option) => option.value === event.target.value
|
||||
)
|
||||
if (selectedOption) {
|
||||
onChange(selectedOption)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<select
|
||||
className="w-full px-3 py-2 border rounded appearance-none"
|
||||
value={value?.value || ''}
|
||||
onChange={handleChange}
|
||||
aria-label={placeholder}
|
||||
>
|
||||
{!value && placeholder && (
|
||||
<option value="" disabled>
|
||||
{placeholder}
|
||||
</option>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Dropdown Component
|
||||
|
||||
## Overview
|
||||
This component was migrated from the original Laconic repository.
|
||||
|
||||
## Usage
|
||||
```tsx
|
||||
import { Dropdown } from '@/components/dropdown';
|
||||
|
||||
// Example usage
|
||||
<Dropdown />
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Dropdown'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface Option {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface DropdownProps {
|
||||
options: Option[]
|
||||
onChange: (option: Option) => void
|
||||
placeholder?: string
|
||||
value?: Option
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { intervalToDuration } from 'date-fns'
|
||||
import React from 'react'
|
||||
import type { FormatMilliSecondProps } from './types'
|
||||
|
||||
/**
|
||||
* A component that formats a given time in milliseconds into a human-readable format.
|
||||
*
|
||||
* @component
|
||||
* @param {FormatMilliSecondProps} props - The props for the FormatMilliSecond component.
|
||||
* @returns {React.ReactElement} A formatted time element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <FormatMilliSecond time={3600000} />
|
||||
* ```
|
||||
*/
|
||||
export const FormatMilliSecond = ({
|
||||
time,
|
||||
...props
|
||||
}: FormatMilliSecondProps) => {
|
||||
const duration = intervalToDuration({ start: 0, end: time })
|
||||
|
||||
return (
|
||||
<div {...props} className="text-sm text-elements-mid-em">
|
||||
{duration.days !== 0 && <span>{duration.days}d </span>}
|
||||
{duration.hours !== 0 && <span>{duration.hours}h </span>}
|
||||
{duration.minutes !== 0 && <span>{duration.minutes}m </span>}
|
||||
<span>{duration.seconds}s</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# FormatMilliSecond Component
|
||||
|
||||
## Overview
|
||||
This component was migrated from the original Laconic repository.
|
||||
|
||||
## Usage
|
||||
```tsx
|
||||
import { FormatMilliSecond } from '@/components/formatmillisecond';
|
||||
|
||||
// Example usage
|
||||
<FormatMilliSecond />
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './FormatMilliSecond'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { ComponentPropsWithoutRef } from 'react'
|
||||
|
||||
/**
|
||||
* Props for the FormatMillisecond component.
|
||||
* @interface FormatMilliSecondProps
|
||||
* @property {number} time - The time in milliseconds to format.
|
||||
*/
|
||||
export interface FormatMilliSecondProps
|
||||
extends ComponentPropsWithoutRef<'div'> {
|
||||
time: number
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import type { LogoProps } from './types'
|
||||
|
||||
/**
|
||||
* A component that renders the Laconic logo with a link to the organization's page.
|
||||
*
|
||||
* @component
|
||||
* @param {LogoProps} props - The props for the Logo component.
|
||||
* @returns {React.ReactElement} A logo element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Logo orgSlug="my-organization" />
|
||||
* ```
|
||||
*/
|
||||
export const Logo = ({ orgSlug }: LogoProps) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/${orgSlug || ''}`}
|
||||
className="flex items-center gap-3 px-0 lg:px-2"
|
||||
>
|
||||
<img src="/logo.svg" alt="Laconic Logo" />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Logo Component
|
||||
|
||||
## Overview
|
||||
This component was migrated from the original Laconic repository.
|
||||
|
||||
## Usage
|
||||
```tsx
|
||||
import { Logo } from '@/components/logo';
|
||||
|
||||
// Example usage
|
||||
<Logo />
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Logo'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Props for the Logo component.
|
||||
* @interface LogoProps
|
||||
* @property {string} [orgSlug] - The organization slug used for the link.
|
||||
*/
|
||||
export interface LogoProps {
|
||||
orgSlug?: string
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# SearchBar Component
|
||||
|
||||
## Overview
|
||||
This component was migrated from the original Laconic repository.
|
||||
|
||||
## Usage
|
||||
```tsx
|
||||
import { SearchBar } from '@/components/searchbar';
|
||||
|
||||
// Example usage
|
||||
<SearchBar />
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { forwardRef } from 'react'
|
||||
import type { SearchBarProps } from './types'
|
||||
|
||||
/**
|
||||
* A search bar component with an icon input.
|
||||
*
|
||||
* @component
|
||||
* @param {SearchBarProps} props - The props for the SearchBar component.
|
||||
* @returns {React.ReactElement} A search bar element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <SearchBar value="search term" onChange={(e) => console.log(e.target.value)} />
|
||||
* ```
|
||||
*/
|
||||
export const SearchBar = forwardRef<HTMLInputElement, SearchBarProps>(
|
||||
({ value, onChange, placeholder = 'Search', ...props }, ref) => {
|
||||
return (
|
||||
<div className="relative flex w-full">
|
||||
<div className="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400">
|
||||
{/* Search icon SVG */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
ref={ref}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
type="search"
|
||||
placeholder={placeholder}
|
||||
className="w-full pl-8 px-3 py-2 border rounded lg:w-[459px]"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './SearchBar'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface SearchBarProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
placeholder?: string
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Stepper Component
|
||||
|
||||
## Overview
|
||||
This component was migrated from the original Laconic repository.
|
||||
|
||||
## Usage
|
||||
```tsx
|
||||
import { Stepper } from '@/components/stepper';
|
||||
|
||||
// Example usage
|
||||
<Stepper />
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
import { StepperNav } from '../vertical-stepper/VerticalStepper'
|
||||
import type { StepperProps, StepperValue } from './types'
|
||||
|
||||
const COLOR_COMPLETED = '#059669'
|
||||
const COLOR_ACTIVE = '#CFE6FC'
|
||||
const COLOR_NOT_STARTED = '#F1F5F9'
|
||||
|
||||
/**
|
||||
* A stepper component that displays a series of steps with different states.
|
||||
*
|
||||
* @component
|
||||
* @param {StepperProps} props - The props for the Stepper component.
|
||||
* @returns {React.ReactElement} A stepper element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Stepper activeStep={1} stepperValues={[{ step: 1, route: '/step1', label: 'Step 1' }]} />
|
||||
* ```
|
||||
*/
|
||||
export const Stepper = ({ activeStep, stepperValues }: StepperProps) => {
|
||||
return (
|
||||
<StepperNav
|
||||
steps={stepperValues.map((stepperValue: StepperValue) => {
|
||||
return {
|
||||
stepContent: () => (
|
||||
<div
|
||||
className={`text-sm ${
|
||||
activeStep === stepperValue.step
|
||||
? 'text-black font-semibold dark:text-foreground'
|
||||
: 'text-gray-600 dark:text-foreground-secondary'
|
||||
}`}
|
||||
>
|
||||
{stepperValue.label}
|
||||
</div>
|
||||
),
|
||||
stepStatusCircleSize: 30,
|
||||
stepStateColor:
|
||||
activeStep > stepperValue.step
|
||||
? COLOR_COMPLETED
|
||||
: activeStep === stepperValue.step
|
||||
? COLOR_ACTIVE
|
||||
: COLOR_NOT_STARTED
|
||||
}
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Stepper'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Represents a step in the stepper.
|
||||
* @interface StepperValue
|
||||
* @property {number} step - The step number.
|
||||
* @property {string} route - The route associated with the step.
|
||||
* @property {string} label - The label for the step.
|
||||
*/
|
||||
export interface StepperValue {
|
||||
step: number
|
||||
route: string
|
||||
label: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the Stepper component.
|
||||
* @interface StepperProps
|
||||
* @property {number} activeStep - The currently active step.
|
||||
* @property {StepperValue[]} stepperValues - The values for each step.
|
||||
*/
|
||||
export interface StepperProps {
|
||||
activeStep: number
|
||||
stepperValues: StepperValue[]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# StopWatch Component
|
||||
|
||||
## Overview
|
||||
This component was migrated from the original Laconic repository.
|
||||
|
||||
## Usage
|
||||
```tsx
|
||||
import { StopWatch } from '@/components/stopwatch';
|
||||
|
||||
// Example usage
|
||||
<StopWatch />
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { FormatMilliSecond } from '../format-milli-second'
|
||||
import type { StopwatchProps } from './types'
|
||||
|
||||
export const setStopWatchOffset = (time: string) => {
|
||||
const providedTime = new Date(time)
|
||||
const currentTime = new Date()
|
||||
const timeDifference = currentTime.getTime() - providedTime.getTime()
|
||||
currentTime.setMilliseconds(currentTime.getMilliseconds() + timeDifference)
|
||||
return currentTime
|
||||
}
|
||||
|
||||
/**
|
||||
* A stopwatch component that tracks elapsed time.
|
||||
*
|
||||
* @component
|
||||
* @param {StopwatchProps} props - The props for the Stopwatch component.
|
||||
* @returns {React.ReactElement} A stopwatch element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <StopWatch offsetTimestamp={new Date()} isPaused={false} />
|
||||
* ```
|
||||
*/
|
||||
export const StopWatch = ({
|
||||
offsetTimestamp,
|
||||
isPaused,
|
||||
...props
|
||||
}: StopwatchProps) => {
|
||||
const [elapsedTime, setElapsedTime] = useState(0)
|
||||
const intervalRef = useRef<number | null>(null)
|
||||
const startTimeRef = useRef(offsetTimestamp.getTime())
|
||||
|
||||
// Set start time when offsetTimestamp changes
|
||||
useEffect(() => {
|
||||
startTimeRef.current = offsetTimestamp.getTime()
|
||||
}, [offsetTimestamp])
|
||||
|
||||
// Handle timer start/stop based on isPaused state
|
||||
useEffect(() => {
|
||||
// Clear any existing interval
|
||||
if (intervalRef.current !== null) {
|
||||
window.clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
|
||||
if (!isPaused) {
|
||||
// Start the timer
|
||||
intervalRef.current = window.setInterval(() => {
|
||||
const now = Date.now()
|
||||
const elapsed = now - startTimeRef.current
|
||||
setElapsedTime(elapsed)
|
||||
}, 1000) // Update every second
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
if (intervalRef.current !== null) {
|
||||
window.clearInterval(intervalRef.current)
|
||||
}
|
||||
}
|
||||
}, [isPaused]) // Only re-run when isPaused changes
|
||||
|
||||
return <FormatMilliSecond time={elapsedTime} {...props} />
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './StopWatch'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { FormatMilliSecondProps } from '../format-milli-second'
|
||||
|
||||
/**
|
||||
* Props for the Stopwatch component.
|
||||
* @interface StopwatchProps
|
||||
* @property {Date} offsetTimestamp - The initial timestamp for the stopwatch.
|
||||
* @property {boolean} isPaused - Whether the stopwatch is paused.
|
||||
*/
|
||||
export interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
||||
offsetTimestamp: Date
|
||||
isPaused: boolean
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# VerticalStepper Component
|
||||
|
||||
## Overview
|
||||
This component was migrated from the original Laconic repository.
|
||||
|
||||
## Usage
|
||||
```tsx
|
||||
import { VerticalStepper } from '@/components/verticalstepper';
|
||||
|
||||
// Example usage
|
||||
<VerticalStepper />
|
||||
```
|
||||
@@ -0,0 +1,103 @@
|
||||
import type React from 'react'
|
||||
import type { ISeparator, IStep, IStepperNavProps } from './types'
|
||||
|
||||
/**
|
||||
* A navigation component for displaying steps in a vertical layout.
|
||||
*
|
||||
* @component
|
||||
* @param {IStepperNavProps} props - The props for the StepperNav component.
|
||||
* @returns {React.ReactElement} A stepper navigation element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <StepperNav steps={[{ stepContent: () => <div>Step 1</div> }]} />
|
||||
* ```
|
||||
*/
|
||||
export const StepperNav = (props: IStepperNavProps): JSX.Element => {
|
||||
return (
|
||||
<nav>
|
||||
{props.steps.map(
|
||||
(
|
||||
{ stepContent, stepStateColor, onClickHandler, stepStatusCircleSize },
|
||||
stepIndex
|
||||
) => (
|
||||
<div key={`step-${stepContent().toString()}-${stepIndex}`}>
|
||||
<Step
|
||||
stepContent={stepContent}
|
||||
statusColor={stepStateColor}
|
||||
onClickHandler={onClickHandler}
|
||||
statusCircleSize={stepStatusCircleSize}
|
||||
/>
|
||||
{stepIndex !== props.steps.length - 1 && (
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: `${Math.floor((stepStatusCircleSize ?? 16) / 2) + 1}px`
|
||||
}}
|
||||
>
|
||||
<Separator />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A separator component for the vertical stepper.
|
||||
*
|
||||
* @component
|
||||
* @param {ISeparator} props - The props for the Separator component.
|
||||
* @returns {React.ReactElement} A separator element.
|
||||
*/
|
||||
export const Separator = ({ height }: ISeparator): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className="h-[5vh] w-0.5 border border-gray-200 bg-gray-200"
|
||||
style={{ height: height ?? '5vh' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A step component for the vertical stepper.
|
||||
*
|
||||
* @component
|
||||
* @param {IStep} props - The props for the Step component.
|
||||
* @returns {React.ReactElement} A step element.
|
||||
*/
|
||||
export const Step = ({
|
||||
stepContent,
|
||||
statusColor,
|
||||
statusCircleSize,
|
||||
onClickHandler
|
||||
}: IStep): JSX.Element => {
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
onClickHandler?.()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClickHandler}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="inline-flex flex-wrap gap-3 p-0.5 cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="rounded-full border-2 border-gray-200"
|
||||
style={{
|
||||
width: statusCircleSize ?? 16,
|
||||
height: statusCircleSize ?? 16,
|
||||
background: statusColor ?? 'white',
|
||||
borderRadius: '50%'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="pb-0.5">{stepContent()}</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './types'
|
||||
export * from './VerticalStepper'
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Describes a step in the stepper navigation.
|
||||
* @interface IStepDescription
|
||||
* @property {() => JSX.Element} stepContent - The content of the step.
|
||||
* @property {string} [stepStateColor] - The color representing the step's state.
|
||||
* @property {number} [stepStatusCircleSize] - The size of the status circle.
|
||||
* @property {() => void} [onClickHandler] - Handler for click events on the step.
|
||||
*/
|
||||
export interface IStepDescription {
|
||||
stepContent: () => JSX.Element
|
||||
stepStateColor?: string
|
||||
stepStatusCircleSize?: number
|
||||
onClickHandler?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the StepperNav component.
|
||||
* @interface IStepperNavProps
|
||||
* @property {IStepDescription[]} steps - The steps to display in the navigation.
|
||||
*/
|
||||
export interface IStepperNavProps {
|
||||
steps: IStepDescription[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the Separator component.
|
||||
* @interface ISeparator
|
||||
* @property {string | number} [height] - The height of the separator.
|
||||
*/
|
||||
export interface ISeparator {
|
||||
height?: string | number
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the Step component.
|
||||
* @interface IStep
|
||||
* @property {() => JSX.Element} stepContent - The content of the step.
|
||||
* @property {string} [statusColor] - The color of the status circle.
|
||||
* @property {number} [statusCircleSize] - The size of the status circle.
|
||||
* @property {() => void} [onClickHandler] - Handler for click events on the step.
|
||||
*/
|
||||
export interface IStep {
|
||||
stepContent: () => JSX.Element
|
||||
statusColor?: string
|
||||
statusCircleSize?: number
|
||||
onClickHandler?: () => void
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Globe } from 'lucide-react'
|
||||
|
||||
/**
|
||||
* Coming Soon overlay component
|
||||
* Displays a message indicating a feature is not yet available
|
||||
*
|
||||
* @param props.message - Optional custom message to display
|
||||
* @param props.routerAction - Optional router action to perform when the button is clicked
|
||||
*/
|
||||
interface ComingSoonOverlayProps {
|
||||
message?: string
|
||||
routerAction?: () => void
|
||||
buttonText?: string
|
||||
}
|
||||
|
||||
export function ComingSoonOverlay({
|
||||
message = 'This feature will be available in the next release.',
|
||||
routerAction,
|
||||
buttonText = 'Get Notified'
|
||||
}: ComingSoonOverlayProps) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-background/35 backdrop-blur-sm">
|
||||
<div className="text-center space-y-4 max-w-md px-4 bg-background/80 p-8 rounded-lg shadow-lg">
|
||||
<Globe className="h-16 w-16 mx-auto text-primary" />
|
||||
<h1 className="text-4xl font-bold tracking-tight">Coming Soon</h1>
|
||||
<p className="text-lg text-muted-foreground">{message}</p>
|
||||
{routerAction && <Button onClick={routerAction}>{buttonText}</Button>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ComingSoonOverlay'
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { FC } from 'react'
|
||||
import type { GitHubSessionButtonProps } from './types'
|
||||
|
||||
/**
|
||||
* GitHubSessionButton component
|
||||
*/
|
||||
export const GitHubSessionButton: FC<GitHubSessionButtonProps> = (props) => {
|
||||
return <div>{/* Component implementation will be migrated here */}</div>
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# GitHubSessionButton Component
|
||||
|
||||
## Overview
|
||||
This component was migrated from the original Laconic repository.
|
||||
|
||||
## Usage
|
||||
```tsx
|
||||
import { GitHubSessionButton } from '@/components/githubsessionbutton';
|
||||
|
||||
// Example usage
|
||||
<GitHubSessionButton />
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './GitHubSessionButton'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { Button } from '@workspace/ui/components/button'
|
||||
|
||||
export type GitHubSessionButtonProps = typeof Button
|
||||
@@ -0,0 +1,17 @@
|
||||
// Exporting all components and types from the foundation directory
|
||||
|
||||
// Page Header
|
||||
export * from './page-header'
|
||||
|
||||
// Page Wrapper
|
||||
export * from './page-wrapper'
|
||||
|
||||
// Navigation Wrapper
|
||||
export * from './navigation-wrapper'
|
||||
|
||||
// Top Navigation
|
||||
export * from './coming-soon-overlay'
|
||||
export * from './top-navigation/dark-mode-toggle'
|
||||
export * from './top-navigation/main-navigation'
|
||||
export * from './top-navigation/navigation-item'
|
||||
export * from './top-navigation/wallet-session-badge'
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { FC } from 'react'
|
||||
import type { LaconicIconProps } from './types'
|
||||
|
||||
export const LaconicIcon: FC<LaconicIconProps> = ({
|
||||
className = '',
|
||||
width = 40,
|
||||
height = 40
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 40 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.7179 20.6493C14.6275 17.7397 16.4284 13.7219 16.4276 9.28566C16.4288 8.68351 16.3955 8.08808 16.3294 7.5L7.5 7.5008L7.50026 24.4654C7.49946 26.5218 8.28351 28.5788 9.85175 30.1469C11.4201 31.7151 13.4785 32.5001 15.5353 32.4991L32.5 32.5L32.4994 23.6694C31.9126 23.6048 31.3171 23.5713 30.7136 23.5711C26.2786 23.5718 22.2605 25.3725 19.351 28.2819C17.2337 30.346 13.8392 30.3464 11.7483 28.2554C9.65859 26.1656 9.65764 22.7701 11.7179 20.6493ZM30.6686 9.33579C28.2303 6.89759 24.2689 6.89665 21.8298 9.33579C19.3906 11.7748 19.3916 15.7361 21.8298 18.1743C24.2694 20.6138 28.2295 20.6134 30.6686 18.1743C33.1078 15.7353 33.1083 11.7752 30.6686 9.33579Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# LaconicIcon Component
|
||||
|
||||
## Overview
|
||||
This component was migrated from the original Laconic repository.
|
||||
|
||||
## Usage
|
||||
```tsx
|
||||
import { LaconicIcon } from '@/components/laconicicon';
|
||||
|
||||
// Example usage
|
||||
<LaconicIcon />
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './LaconicIcon'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* LaconicIconProps interface defines the props for the LaconicIcon component.
|
||||
*/
|
||||
export interface LaconicIconProps {
|
||||
/**
|
||||
* Optional CSS class names to apply to the component.
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
* The width of the icon.
|
||||
* @default 40
|
||||
*/
|
||||
width?: number
|
||||
/**
|
||||
* The height of the icon.
|
||||
* @default 40
|
||||
*/
|
||||
height?: number
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import { LaconicMark } from '@/components/assets/laconic-mark'
|
||||
import { cn } from '@workspace/ui/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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# LoadingOverlay Component
|
||||
|
||||
A component to display a loading overlay with optional logo and spinner.
|
||||
|
||||
## Features
|
||||
|
||||
- Displays a loading overlay
|
||||
- Optional Laconic logo and spinner
|
||||
- Customizable styling and z-index
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { LoadingOverlay } from 'path/to/components/foundation/loading/loading-overlay'
|
||||
|
||||
// Basic usage
|
||||
<LoadingOverlay isLoading={true} />
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `isLoading` | `boolean` | Controls the visibility of the overlay |
|
||||
| `className` | `string` | Optional className for styling |
|
||||
| `showLogo` | `boolean` | Whether to show the Laconic logo |
|
||||
| `showSpinner` | `boolean` | Whether to show the loading spinner |
|
||||
| `zIndex` | `number` | The z-index value for the overlay |
|
||||
| `solid` | `boolean` | Whether to use a solid black background |
|
||||
@@ -0,0 +1 @@
|
||||
export { LoadingOverlay } from './LoadingOverlay'
|
||||
@@ -0,0 +1,225 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* Props for the NavigationWrapper component
|
||||
* @remarks
|
||||
* Configuration interface for NavigationWrapper, a layout component that provides:
|
||||
* - Full-height, full-width container with flex column layout
|
||||
* - Support for top navigation bar (currently commented out)
|
||||
* - Flexible content area for page content
|
||||
* - Customizable styling through className prop
|
||||
*
|
||||
* @see {@link NavigationWrapper} for the component implementation
|
||||
*/
|
||||
export interface NavigationWrapperProps {
|
||||
/**
|
||||
* Main content for the navigation wrapper
|
||||
* @remarks
|
||||
* - Typically contains {@link PageWrapper} components
|
||||
* - Rendered in the main content area below navigation
|
||||
* - Can include any valid React nodes
|
||||
* - Takes up remaining vertical space
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Basic usage with PageWrapper
|
||||
* <NavigationWrapper>
|
||||
* <PageWrapper header={{ title: "Dashboard" }}>
|
||||
* <DashboardContent />
|
||||
* </PageWrapper>
|
||||
* </NavigationWrapper>
|
||||
*
|
||||
* // Multiple pages in tabs/routes
|
||||
* <NavigationWrapper>
|
||||
* {selectedTab === 'dashboard' && (
|
||||
* <PageWrapper header={{ title: "Dashboard" }}>
|
||||
* <DashboardContent />
|
||||
* </PageWrapper>
|
||||
* )}
|
||||
* {selectedTab === 'settings' && (
|
||||
* <PageWrapper header={{ title: "Settings" }}>
|
||||
* <SettingsContent />
|
||||
* </PageWrapper>
|
||||
* )}
|
||||
* </NavigationWrapper>
|
||||
* ```
|
||||
*/
|
||||
children: ReactNode
|
||||
|
||||
/**
|
||||
* Optional CSS classes for the wrapper
|
||||
* @remarks
|
||||
* - Applied to the wrapper's root container
|
||||
* - Combined with default classes using the cn utility
|
||||
* - Default classes: 'flex flex-col min-h-screen w-full'
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Custom background
|
||||
* className="bg-background"
|
||||
*
|
||||
* // Custom max width
|
||||
* className="max-w-7xl mx-auto"
|
||||
*
|
||||
* // Custom padding
|
||||
* className="px-4 md:px-6"
|
||||
* ```
|
||||
*/
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A layout component that provides navigation structure and content organization.
|
||||
*
|
||||
* @description
|
||||
* NavigationWrapper is a foundational layout component that:
|
||||
* - Creates a full-height, full-width container
|
||||
* - Supports top navigation (implementation commented out)
|
||||
* - Provides flexible content area for page content
|
||||
* - Typically wraps {@link PageWrapper} components
|
||||
*
|
||||
* @keywords layout, navigation, container, foundation-component
|
||||
* @category Layout
|
||||
* @scope Foundation
|
||||
*
|
||||
* @usage
|
||||
* Common patterns:
|
||||
*
|
||||
* Basic app layout:
|
||||
* ```tsx
|
||||
* // In app/layout.tsx
|
||||
* export default function RootLayout({ children }) {
|
||||
* return (
|
||||
* <html lang="en">
|
||||
* <body>
|
||||
* <NavigationWrapper>
|
||||
* {children}
|
||||
* </NavigationWrapper>
|
||||
* </body>
|
||||
* </html>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* With custom styling:
|
||||
* ```tsx
|
||||
* <NavigationWrapper className="bg-background">
|
||||
* <PageWrapper>
|
||||
* <Content />
|
||||
* </PageWrapper>
|
||||
* </NavigationWrapper>
|
||||
* ```
|
||||
*
|
||||
* With route-based content:
|
||||
* ```tsx
|
||||
* <NavigationWrapper>
|
||||
* <Routes>
|
||||
* <Route
|
||||
* path="/dashboard"
|
||||
* element={
|
||||
* <PageWrapper header={{ title: "Dashboard" }}>
|
||||
* <DashboardContent />
|
||||
* </PageWrapper>
|
||||
* }
|
||||
* />
|
||||
* <Route
|
||||
* path="/settings"
|
||||
* element={
|
||||
* <PageWrapper header={{ title: "Settings" }}>
|
||||
* <SettingsContent />
|
||||
* </PageWrapper>
|
||||
* }
|
||||
* />
|
||||
* </Routes>
|
||||
* </NavigationWrapper>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Basic usage
|
||||
* <NavigationWrapper>
|
||||
* <PageWrapper>
|
||||
* <div>Page content</div>
|
||||
* </PageWrapper>
|
||||
* </NavigationWrapper>
|
||||
*
|
||||
* // With custom styling
|
||||
* <NavigationWrapper className="bg-background">
|
||||
* <PageWrapper>
|
||||
* <div>Styled content</div>
|
||||
* </PageWrapper>
|
||||
* </NavigationWrapper>
|
||||
* ```
|
||||
*
|
||||
* @param props - Component props
|
||||
* @param props.children - Main content to be rendered within the wrapper
|
||||
* @param props.className - Additional CSS classes for the root container
|
||||
*
|
||||
* @returns A layout wrapper with navigation structure
|
||||
*
|
||||
* @related {@link PageWrapper} - Commonly wrapped by NavigationWrapper
|
||||
* @composition Uses {@link cn} for class name merging
|
||||
*
|
||||
* @cssUtilities
|
||||
* - flex-col: Column layout
|
||||
* - min-h-screen: Minimum full viewport height
|
||||
* - w-full: Full width
|
||||
*
|
||||
* @accessibility
|
||||
* - Maintains semantic HTML structure
|
||||
* - Preserves content hierarchy
|
||||
* - Supports keyboard navigation (when nav is implemented)
|
||||
*
|
||||
* @performance
|
||||
* - Minimal DOM nesting
|
||||
* - Uses utility classes for styling
|
||||
* - Conditional navigation rendering
|
||||
*/
|
||||
export default function NavigationWrapper({
|
||||
children,
|
||||
className
|
||||
}: NavigationWrapperProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col min-h-screen w-full', className)}>
|
||||
{/* Top Navigation */}
|
||||
{/* <header className="sticky top-0 z-40 w-full border-b bg-background">
|
||||
<div className="container flex h-16 items-center">
|
||||
<div className="mr-4 flex">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<span className="font-bold text-xl">App Logo</span>
|
||||
</Link>
|
||||
</div>
|
||||
<nav className="flex items-center space-x-4 lg:space-x-6 mx-6">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-sm font-medium transition-colors hover:text-primary"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="/projects"
|
||||
className="text-sm font-medium transition-colors hover:text-primary"
|
||||
>
|
||||
Projects
|
||||
</Link>
|
||||
<Link
|
||||
href="/settings"
|
||||
className="text-sm font-medium transition-colors hover:text-primary"
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="ml-auto flex items-center space-x-4">
|
||||
<Button className="text-sm font-medium">Profile</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header> */}
|
||||
|
||||
{/* Main Content */}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# NavigationWrapper Component
|
||||
|
||||
A container component that wraps page content with navigation functionality.
|
||||
|
||||
## Features
|
||||
|
||||
- Wraps content with navigation
|
||||
- Customizable styling
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { NavigationWrapper } from 'path/to/components/foundation/navigation-wrapper'
|
||||
|
||||
// Basic usage
|
||||
<NavigationWrapper className="custom-class">
|
||||
<div>Content with navigation</div>
|
||||
</NavigationWrapper>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
See [types.ts](./types.ts) for detailed type definitions.
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `children` | `ReactNode` | Content to be displayed within the navigation wrapper |
|
||||
| `className` | `string` | Additional CSS classes |
|
||||
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
default as NavigationWrapper,
|
||||
type NavigationWrapperProps
|
||||
} from './NavigationWrapper'
|
||||
@@ -0,0 +1,343 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@workspace/ui/components/dropdown-menu'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import { MoreVertical } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* Configuration for header action buttons/links
|
||||
* @remarks
|
||||
* Interactive elements with configurable styling and behavior:
|
||||
* - Use onClick for JS actions OR href for navigation (not both)
|
||||
* - Multiple visual styles via variant prop
|
||||
* - Optional primary emphasis for main call-to-action
|
||||
*/
|
||||
export interface PageAction {
|
||||
/**
|
||||
* Display text for the action button/link
|
||||
* @remarks Shown as the button/link text content
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* Visual style variant for the button
|
||||
* @remarks
|
||||
* Available styles:
|
||||
* - `default`: Standard appearance
|
||||
* - `destructive`: Dangerous actions
|
||||
* - `outline`: Bordered, transparent bg
|
||||
* - `secondary`: Less prominent
|
||||
* - `ghost`: Minimal styling
|
||||
* - `link`: Hyperlink style
|
||||
* @default 'default'
|
||||
*/
|
||||
variant?:
|
||||
| 'default' // Standard appearance
|
||||
| 'destructive' // Dangerous actions
|
||||
| 'outline' // Bordered, transparent bg
|
||||
| 'secondary' // Less prominent
|
||||
| 'ghost' // Minimal styling
|
||||
| 'link' // Hyperlink style
|
||||
|
||||
/**
|
||||
* Click handler for button-based actions
|
||||
* @remarks
|
||||
* - Use for JavaScript-triggered actions
|
||||
* - Mutually exclusive with `href`
|
||||
*/
|
||||
onClick?: () => void
|
||||
|
||||
/**
|
||||
* URL for link-based actions
|
||||
* @remarks
|
||||
* - Use for navigation to new URLs
|
||||
* - Mutually exclusive with `onClick`
|
||||
*/
|
||||
href?: string
|
||||
|
||||
/**
|
||||
* Whether this action should have primary visual emphasis
|
||||
* @remarks
|
||||
* - When true, applies prominent styling
|
||||
* - Useful for main call-to-action buttons
|
||||
* - Affects mobile layout (primary actions shown, secondary in dropdown)
|
||||
* @default false
|
||||
*/
|
||||
isPrimary?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the PageHeader component
|
||||
* @remarks
|
||||
* Configuration interface for PageHeader, providing:
|
||||
* - Required title as main heading
|
||||
* - Optional subtitle for additional context
|
||||
* - Optional action buttons/links
|
||||
* - Responsive layout with mobile optimization
|
||||
* - Customizable styling
|
||||
*/
|
||||
export interface PageHeaderProps {
|
||||
/**
|
||||
* Main heading text
|
||||
* @remarks
|
||||
* - Rendered as h1 element
|
||||
* - Responsive text size (2xl on mobile, 30px on desktop)
|
||||
* - Bold weight with consistent line height
|
||||
*/
|
||||
title: string
|
||||
|
||||
/**
|
||||
* Additional content below the title
|
||||
* @remarks
|
||||
* - Can be plain text or custom component
|
||||
* - Text is muted and slightly smaller
|
||||
* - Components receive full width
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Text subtitle
|
||||
* subtitle="Optional description"
|
||||
*
|
||||
* // Component subtitle
|
||||
* subtitle={<SearchInput onChange={handleSearch} />}
|
||||
* ```
|
||||
*/
|
||||
subtitle?: string | ReactNode
|
||||
|
||||
/**
|
||||
* Array of action buttons/links
|
||||
* @remarks
|
||||
* - Desktop: All actions shown in a row
|
||||
* - Mobile: Primary actions shown, secondary in dropdown
|
||||
* - Actions can be buttons (onClick) or links (href)
|
||||
* - Support multiple visual styles via variant prop
|
||||
*
|
||||
* @see {@link PageAction} for detailed action configuration
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* actions={[
|
||||
* {
|
||||
* label: "Create New",
|
||||
* isPrimary: true,
|
||||
* onClick: () => setOpen(true)
|
||||
* },
|
||||
* {
|
||||
* label: "View All",
|
||||
* href: "/items",
|
||||
* variant: "outline"
|
||||
* }
|
||||
* ]}
|
||||
* ```
|
||||
*/
|
||||
actions?: PageAction[]
|
||||
|
||||
/**
|
||||
* Optional CSS classes
|
||||
* @remarks
|
||||
* - Applied to the header's root container
|
||||
* - Combined with default classes using cn utility
|
||||
* - Default max-width of 1232px with auto margins
|
||||
*/
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A responsive page header component with title, subtitle, and actions.
|
||||
*
|
||||
* @description
|
||||
* PageHeader provides a consistent header structure with:
|
||||
* - Prominent title as h1
|
||||
* - Optional subtitle or custom component
|
||||
* - Configurable action buttons/links
|
||||
* - Responsive layout with mobile optimization
|
||||
* - Customizable styling
|
||||
*
|
||||
* @keywords header, page-title, action-buttons, responsive-header, foundation-component
|
||||
* @category Layout
|
||||
* @scope Foundation
|
||||
*
|
||||
* @usage
|
||||
* Common patterns:
|
||||
*
|
||||
* Basic title only:
|
||||
* ```tsx
|
||||
* <PageHeader title="Dashboard" />
|
||||
* ```
|
||||
*
|
||||
* With subtitle and primary action:
|
||||
* ```tsx
|
||||
* <PageHeader
|
||||
* title="Projects"
|
||||
* subtitle="Your active projects"
|
||||
* actions={[
|
||||
* { label: "New Project", isPrimary: true, onClick: handleCreate }
|
||||
* ]}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* With search component and multiple actions:
|
||||
* ```tsx
|
||||
* <PageHeader
|
||||
* title="Team Members"
|
||||
* subtitle={<SearchInput placeholder="Search members..." />}
|
||||
* actions={[
|
||||
* { label: "Invite", isPrimary: true, onClick: handleInvite },
|
||||
* { label: "Export", variant: "outline", onClick: handleExport },
|
||||
* { label: "Settings", href: "/team/settings", variant: "ghost" }
|
||||
* ]}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* With navigation actions:
|
||||
* ```tsx
|
||||
* <PageHeader
|
||||
* title="Edit Profile"
|
||||
* actions={[
|
||||
* { label: "Save", isPrimary: true, onClick: handleSave },
|
||||
* { label: "Cancel", href: "/profile", variant: "ghost" }
|
||||
* ]}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <PageHeader
|
||||
* title="Dashboard"
|
||||
* subtitle="Welcome back!"
|
||||
* actions={[
|
||||
* {
|
||||
* label: "New Item",
|
||||
* isPrimary: true,
|
||||
* onClick: () => console.log("clicked")
|
||||
* }
|
||||
* ]}
|
||||
* className="mb-8"
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @related {@link PageWrapper} - Often used together for page layout
|
||||
* @related {@link Button} - Used for rendering actions
|
||||
* @composition Uses {@link DropdownMenu} for mobile action menu
|
||||
*
|
||||
* @cssUtilities
|
||||
* - flex-col/flex-row: Responsive layout
|
||||
* - gap-6/gap-2: Consistent spacing
|
||||
* - text-2xl/text-[30px]: Responsive typography
|
||||
* - text-foreground/text-muted-foreground: Text hierarchy
|
||||
*
|
||||
* @accessibility
|
||||
* - Uses semantic h1 for title
|
||||
* - Maintains text contrast ratios
|
||||
* - Dropdown menu is keyboard navigable
|
||||
* - Preserves action button/link semantics
|
||||
*
|
||||
* @performance
|
||||
* - Conditional rendering of subtitle and actions
|
||||
* - Mobile-first CSS with responsive modifiers
|
||||
* - Efficient action rendering with key prop
|
||||
*/
|
||||
export default function PageHeader({
|
||||
title,
|
||||
subtitle,
|
||||
actions = [],
|
||||
className
|
||||
}: PageHeaderProps) {
|
||||
// Separate primary actions from secondary actions
|
||||
const primaryActions = actions.filter((action) => action.isPrimary)
|
||||
const secondaryActions = actions.filter((action) => !action.isPrimary)
|
||||
|
||||
// Render an action (either as button or link)
|
||||
const renderAction = (action: PageAction, key: string) => {
|
||||
const variant = action.variant || (action.isPrimary ? 'default' : 'outline')
|
||||
|
||||
if (action.href) {
|
||||
return (
|
||||
<Button key={key} variant={variant} asChild>
|
||||
<Link href={action.href}>{action.label}</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button key={key} variant={variant} onClick={action.onClick}>
|
||||
{action.label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('w-full max-w-[1232px] mx-auto', className)}>
|
||||
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-6 md:gap-2">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl md:text-[30px] font-bold leading-8 md:leading-9 text-foreground">
|
||||
{title}
|
||||
</h1>
|
||||
{subtitle && (
|
||||
<div className="mt-2">
|
||||
{typeof subtitle === 'string' ? (
|
||||
<p className="text-sm md:text-base text-muted-foreground leading-5 md:leading-6">
|
||||
{subtitle}
|
||||
</p>
|
||||
) : (
|
||||
subtitle
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{actions.length > 0 && (
|
||||
<>
|
||||
{/* Desktop buttons */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
{actions.map((action, index) =>
|
||||
renderAction(action, `desktop-${index}`)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile buttons */}
|
||||
<div className="flex md:hidden items-center gap-2">
|
||||
{primaryActions.map((action, index) =>
|
||||
renderAction(action, `mobile-primary-${index}`)
|
||||
)}
|
||||
|
||||
{secondaryActions.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{secondaryActions.map((action) =>
|
||||
action.href ? (
|
||||
<DropdownMenuItem asChild key={action.label}>
|
||||
<Link href={action.href}>{action.label}</Link>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
onClick={action.onClick}
|
||||
key={action.label}
|
||||
>
|
||||
{action.label}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user