Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b97a84671 | ||
|
|
bfce5c860a | ||
|
|
2610e88274 | ||
|
|
4466ec046c | ||
|
|
3235186374 |
@@ -0,0 +1,4 @@
|
||||
.react-router
|
||||
build
|
||||
node_modules
|
||||
README.md
|
||||
@@ -0,0 +1,6 @@
|
||||
.DS_Store
|
||||
/node_modules/
|
||||
|
||||
# React Router
|
||||
/.react-router/
|
||||
/build/
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
FROM node:20-alpine AS development-dependencies-env
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN npm ci
|
||||
|
||||
FROM node:20-alpine AS production-dependencies-env
|
||||
COPY ./package.json package-lock.json /app/
|
||||
WORKDIR /app
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
FROM node:20-alpine AS build-env
|
||||
COPY . /app/
|
||||
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||
WORKDIR /app
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine
|
||||
COPY ./package.json package-lock.json /app/
|
||||
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||
COPY --from=build-env /app/build /app/build
|
||||
WORKDIR /app
|
||||
CMD ["npm", "run", "start"]
|
||||
@@ -0,0 +1,87 @@
|
||||
# Welcome to React Router!
|
||||
|
||||
A modern, production-ready template for building full-stack React applications using React Router.
|
||||
|
||||
[](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 Server-side rendering
|
||||
- ⚡️ Hot Module Replacement (HMR)
|
||||
- 📦 Asset bundling and optimization
|
||||
- 🔄 Data loading and mutations
|
||||
- 🔒 TypeScript by default
|
||||
- 🎉 TailwindCSS for styling
|
||||
- 📖 [React Router docs](https://reactrouter.com/)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
Install the dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
Start the development server with HMR:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Your application will be available at `http://localhost:5173`.
|
||||
|
||||
## Building for Production
|
||||
|
||||
Create a production build:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
To build and run using Docker:
|
||||
|
||||
```bash
|
||||
docker build -t my-app .
|
||||
|
||||
# Run the container
|
||||
docker run -p 3000:3000 my-app
|
||||
```
|
||||
|
||||
The containerized application can be deployed to any platform that supports Docker, including:
|
||||
|
||||
- AWS ECS
|
||||
- Google Cloud Run
|
||||
- Azure Container Apps
|
||||
- Digital Ocean App Platform
|
||||
- Fly.io
|
||||
- Railway
|
||||
|
||||
### DIY Deployment
|
||||
|
||||
If you're familiar with deploying Node applications, the built-in app server is production-ready.
|
||||
|
||||
Make sure to deploy the output of `npm run build`
|
||||
|
||||
```
|
||||
├── package.json
|
||||
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
|
||||
├── build/
|
||||
│ ├── client/ # Static assets
|
||||
│ └── server/ # Server-side code
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
|
||||
|
||||
---
|
||||
|
||||
Built with ❤️ using React Router.
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
type InstanceProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const InstanceProvider: FC<InstanceProviderProps> = observer((props) => {
|
||||
const { children } = props;
|
||||
// store hooks
|
||||
const { fetchInstanceInfo } = useInstance();
|
||||
// fetching instance details
|
||||
useSWR("INSTANCE_DETAILS", () => fetchInstanceInfo(), {
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
errorRetryCount: 0,
|
||||
});
|
||||
|
||||
return <>{children}</>;
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration, type MetaFunction } from "react-router";
|
||||
import type { Route } from "./+types/root";
|
||||
import "@/styles/globals.css";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { ToastWithTheme } from "./toast.provider";
|
||||
import { StoreProvider } from "./store.provider";
|
||||
import { InstanceProvider } from "./instance.provider";
|
||||
import { UserProvider } from "./user.provider";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: "Plane | Simple, extensible, open-source project management tool.",
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
content:
|
||||
"Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.",
|
||||
},
|
||||
{
|
||||
name: "openGraph:title",
|
||||
content: "Plane | Simple, extensible, open-source project management tool.",
|
||||
},
|
||||
{
|
||||
name: "openGraph:description",
|
||||
content:
|
||||
"Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.",
|
||||
},
|
||||
{ name: "openGraph:url", content: "https://plane.so/" },
|
||||
{
|
||||
name: "keywords",
|
||||
content:
|
||||
"software development, customer feedback, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration",
|
||||
},
|
||||
{ name: "twitter:site", content: "@planepowers" },
|
||||
{
|
||||
name: "twitter:title",
|
||||
content: "Plane | Simple, extensible, open-source project management tool.",
|
||||
},
|
||||
{
|
||||
name: "twitter:description",
|
||||
content:
|
||||
"Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.",
|
||||
},
|
||||
{ name: "twitter:url", content: "https://plane.so/" },
|
||||
{ name: "twitter:image", content: "https://plane.so/og-image.png" },
|
||||
{ name: "twitter:image:alt", content: "Plane - Modern project management" },
|
||||
{ name: "twitter:image:width", content: "1200" },
|
||||
{ name: "twitter:image:height", content: "630" },
|
||||
];
|
||||
};
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={`/favicon/apple-touch-icon.png`} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={`/favicon/favicon-32x32.png`} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={`/favicon/favicon-16x16.png`} />
|
||||
<link rel="manifest" href={`/site.webmanifest.json`} />
|
||||
<link rel="shortcut icon" href={`/favicon/favicon.ico`} />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className={`antialiased`}>
|
||||
{/* <ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||
|
||||
</ThemeProvider> */}
|
||||
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
/* <ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||
<ToastWithTheme />
|
||||
<StoreProvider>
|
||||
<InstanceProvider>
|
||||
<UserProvider>{children}</UserProvider>
|
||||
</InstanceProvider>
|
||||
</StoreProvider>
|
||||
</ThemeProvider> */
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
let message = "Oops!";
|
||||
let details = "An unexpected error occurred.";
|
||||
let stack: string | undefined;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
message = error.status === 404 ? "404" : "Error";
|
||||
details = error.status === 404 ? "The requested page could not be found." : error.statusText || details;
|
||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||
details = error.message;
|
||||
stack = error.stack;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="pt-16 p-4 container mx-auto">
|
||||
<h1>{message}</h1>
|
||||
<p>{details}</p>
|
||||
{stack && (
|
||||
<pre className="w-full p-4 overflow-x-auto">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { type RouteConfig } from "@react-router/dev/routes";
|
||||
import { flatRoutes } from "@react-router/fs-routes";
|
||||
|
||||
export default flatRoutes() satisfies RouteConfig;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { InstanceSignInForm } from "@/components/instance/sign-in/form";
|
||||
|
||||
export default function HomePage() {
|
||||
return <InstanceSignInForm />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { InstanceSignInForm } from "@/components/instance/sign-in/form";
|
||||
|
||||
export default function GeneralPage() {
|
||||
return <InstanceSignInForm />;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
export default function GeneralLayout() {
|
||||
return (
|
||||
<div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { createContext } from "react";
|
||||
// plane admin store
|
||||
import { RootStore } from "@/plane-admin/store/root.store";
|
||||
|
||||
let rootStore = new RootStore();
|
||||
|
||||
export const StoreContext = createContext(rootStore);
|
||||
|
||||
function initializeStore(initialData = {}) {
|
||||
const singletonRootStore = rootStore ?? new RootStore();
|
||||
// If your page has Next.js data fetching methods that use a Mobx store, it will
|
||||
// get hydrated here, check `pages/ssg.js` and `pages/ssr.js` for more details
|
||||
if (initialData) {
|
||||
singletonRootStore.hydrate(initialData);
|
||||
}
|
||||
// For SSG and SSR always create a new store
|
||||
if (typeof window === "undefined") return singletonRootStore;
|
||||
// Create the store once in the client
|
||||
if (!rootStore) rootStore = singletonRootStore;
|
||||
return singletonRootStore;
|
||||
}
|
||||
|
||||
export type StoreProviderProps = {
|
||||
children: React.ReactNode;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
initialState?: any;
|
||||
};
|
||||
|
||||
export const StoreProvider = ({ children, initialState = {} }: StoreProviderProps) => {
|
||||
const store = initializeStore(initialState);
|
||||
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
// import { Toast } from "@plane/propel/toast";
|
||||
// import { resolveGeneralTheme } from "@plane/utils";
|
||||
|
||||
export const ToastWithTheme = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
// return <Toast theme={resolveGeneralTheme(resolvedTheme)} />;
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import { useInstance, useTheme, useUser } from "@/hooks/store";
|
||||
|
||||
interface IUserProvider {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const UserProvider: FC<IUserProvider> = observer(({ children }) => {
|
||||
// hooks
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
const { currentUser, fetchCurrentUser } = useUser();
|
||||
const { fetchInstanceAdmins } = useInstance();
|
||||
|
||||
useSWR("CURRENT_USER", () => fetchCurrentUser(), {
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
|
||||
useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins());
|
||||
|
||||
useEffect(() => {
|
||||
// Only access localStorage after component has mounted to prevent hydration mismatch
|
||||
if (typeof window !== "undefined") {
|
||||
const localValue = localStorage.getItem("god_mode_sidebar_collapsed");
|
||||
const localBoolValue = localValue ? (localValue === "true" ? true : false) : false;
|
||||
if (isSidebarCollapsed === undefined && localBoolValue != isSidebarCollapsed) toggleSidebar(localBoolValue);
|
||||
}
|
||||
}, [isSidebarCollapsed, currentUser, toggleSidebar]);
|
||||
|
||||
return <>{children}</>;
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { KeyRound, Mails } from "lucide-react";
|
||||
// types
|
||||
import type {
|
||||
TGetBaseAuthenticationModeProps,
|
||||
TInstanceAuthenticationMethodKeys,
|
||||
TInstanceAuthenticationModes,
|
||||
} from "@plane/types";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
// components
|
||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||
import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch";
|
||||
import { GithubConfiguration } from "@/components/authentication/github-config";
|
||||
import { GitlabConfiguration } from "@/components/authentication/gitlab-config";
|
||||
import { GoogleConfiguration } from "@/components/authentication/google-config";
|
||||
import { PasswordLoginConfiguration } from "@/components/authentication/password-config-switch";
|
||||
// plane admin components
|
||||
import { UpgradeButton } from "@/plane-admin/components/common";
|
||||
// assets
|
||||
import githubLightModeImage from "@/public/logos/github-black.png";
|
||||
import githubDarkModeImage from "@/public/logos/github-white.png";
|
||||
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
|
||||
import GoogleLogo from "@/public/logos/google-logo.svg";
|
||||
import OIDCLogo from "@/public/logos/oidc-logo.svg";
|
||||
import SAMLLogo from "@/public/logos/saml-logo.svg";
|
||||
|
||||
export type TAuthenticationModeProps = {
|
||||
disabled: boolean;
|
||||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
// Authentication methods
|
||||
export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({
|
||||
disabled,
|
||||
updateConfig,
|
||||
resolvedTheme,
|
||||
}) => [
|
||||
{
|
||||
key: "unique-codes",
|
||||
name: "Unique codes",
|
||||
description:
|
||||
"Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
|
||||
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
|
||||
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "passwords-login",
|
||||
name: "Passwords",
|
||||
description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.",
|
||||
icon: <KeyRound className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
|
||||
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "google",
|
||||
name: "Google",
|
||||
description: "Allow members to log in or sign up for Plane with their Google accounts.",
|
||||
icon: <img src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
|
||||
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "github",
|
||||
name: "GitHub",
|
||||
description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
|
||||
icon: (
|
||||
<img
|
||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
|
||||
height={20}
|
||||
width={20}
|
||||
alt="GitHub Logo"
|
||||
/>
|
||||
),
|
||||
config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "gitlab",
|
||||
name: "GitLab",
|
||||
description: "Allow members to log in or sign up to plane with their GitLab accounts.",
|
||||
icon: <img src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
|
||||
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "oidc",
|
||||
name: "OIDC",
|
||||
description: "Authenticate your users via the OpenID Connect protocol.",
|
||||
icon: <img src={OIDCLogo} height={22} width={22} alt="OIDC Logo" />,
|
||||
config: <UpgradeButton />,
|
||||
unavailable: true,
|
||||
},
|
||||
{
|
||||
key: "saml",
|
||||
name: "SAML",
|
||||
description: "Authenticate your users via the Security Assertion Markup Language protocol.",
|
||||
icon: <img src={SAMLLogo} height={22} width={22} alt="SAML Logo" className="pl-0.5" />,
|
||||
config: <UpgradeButton />,
|
||||
unavailable: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const AuthenticationModes: React.FC<TAuthenticationModeProps> = observer((props) => {
|
||||
const { disabled, updateConfig } = props;
|
||||
// next-themes
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
{getAuthenticationModes({ disabled, updateConfig, resolvedTheme }).map((method) => (
|
||||
<AuthenticationMethodCard
|
||||
key={method.key}
|
||||
name={method.name}
|
||||
description={method.description}
|
||||
icon={method.icon}
|
||||
config={method.config}
|
||||
disabled={disabled}
|
||||
unavailable={method.unavailable}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./authentication-modes";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./upgrade-button";
|
||||
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
// icons
|
||||
import { SquareArrowOutUpRight } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { getButtonStyling } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
export const UpgradeButton: React.FC = () => (
|
||||
<a href="https://plane.so/pricing?mode=self-hosted" target="_blank" className={cn(getButtonStyling("primary", "sm"))}>
|
||||
Upgrade
|
||||
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
|
||||
</a>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { enableStaticRendering } from "mobx-react";
|
||||
// stores
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
export class RootStore extends CoreRootStore {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
hydrate(initialData: any) {
|
||||
super.hydrate(initialData);
|
||||
}
|
||||
|
||||
resetOnSignOut() {
|
||||
super.resetOnSignOut();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
config: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
withBorder?: boolean;
|
||||
unavailable?: boolean;
|
||||
};
|
||||
|
||||
export const AuthenticationMethodCard: FC<Props> = (props) => {
|
||||
const { name, description, icon, config, disabled = false, withBorder = true, unavailable = false } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("w-full flex items-center gap-14 rounded", {
|
||||
"px-4 py-3 border border-custom-border-200": withBorder,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={cn("flex grow items-center gap-4", {
|
||||
"opacity-50": unavailable,
|
||||
})}
|
||||
>
|
||||
<div className="shrink-0">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-custom-background-80">{icon}</div>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div
|
||||
className={cn("font-medium leading-5 text-custom-text-100", {
|
||||
"text-sm": withBorder,
|
||||
"text-xl": !withBorder,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div
|
||||
className={cn("font-normal leading-5 text-custom-text-300", {
|
||||
"text-xs": withBorder,
|
||||
"text-sm": !withBorder,
|
||||
})}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`shrink-0 ${disabled && "opacity-70"}`}>{config}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// ui
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
disabled: boolean;
|
||||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
export const EmailCodesConfiguration: React.FC<Props> = observer((props) => {
|
||||
const { disabled, updateConfig } = props;
|
||||
// store
|
||||
const { formattedConfig } = useInstance();
|
||||
// derived values
|
||||
const enableMagicLogin = formattedConfig?.ENABLE_MAGIC_LINK_LOGIN ?? "";
|
||||
|
||||
return (
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableMagicLogin))}
|
||||
onChange={() => {
|
||||
const newEnableMagicLogin = Boolean(parseInt(enableMagicLogin)) === true ? "0" : "1";
|
||||
updateConfig("ENABLE_MAGIC_LINK_LOGIN", newEnableMagicLogin);
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Link } from "react-router";
|
||||
// icons
|
||||
import { Settings2 } from "lucide-react";
|
||||
// plane internal packages
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
disabled: boolean;
|
||||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
export const GithubConfiguration: React.FC<Props> = observer((props) => {
|
||||
const { disabled, updateConfig } = props;
|
||||
// store
|
||||
const { formattedConfig } = useInstance();
|
||||
// derived values
|
||||
const enableGithubConfig = formattedConfig?.IS_GITHUB_ENABLED ?? "";
|
||||
const isGithubConfigured = !!formattedConfig?.GITHUB_CLIENT_ID && !!formattedConfig?.GITHUB_CLIENT_SECRET;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isGithubConfigured ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/authentication/github" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGithubConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGithubConfig = Boolean(parseInt(enableGithubConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GITHUB_ENABLED", newEnableGithubConfig);
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
to="/authentication/github"
|
||||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
||||
>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
||||
Configure
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Link } from "react-router";
|
||||
// icons
|
||||
import { Settings2 } from "lucide-react";
|
||||
// plane internal packages
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
disabled: boolean;
|
||||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
export const GitlabConfiguration: React.FC<Props> = observer((props) => {
|
||||
const { disabled, updateConfig } = props;
|
||||
// store
|
||||
const { formattedConfig } = useInstance();
|
||||
// derived values
|
||||
const enableGitlabConfig = formattedConfig?.IS_GITLAB_ENABLED ?? "";
|
||||
const isGitlabConfigured = !!formattedConfig?.GITLAB_CLIENT_ID && !!formattedConfig?.GITLAB_CLIENT_SECRET;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isGitlabConfigured ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/authentication/gitlab" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGitlabConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGitlabConfig = Boolean(parseInt(enableGitlabConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GITLAB_ENABLED", newEnableGitlabConfig);
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
to="/authentication/gitlab"
|
||||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
||||
>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
||||
Configure
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Link } from "react-router";
|
||||
// icons
|
||||
import { Settings2 } from "lucide-react";
|
||||
// plane internal packages
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
disabled: boolean;
|
||||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
export const GoogleConfiguration: React.FC<Props> = observer((props) => {
|
||||
const { disabled, updateConfig } = props;
|
||||
// store
|
||||
const { formattedConfig } = useInstance();
|
||||
// derived values
|
||||
const enableGoogleConfig = formattedConfig?.IS_GOOGLE_ENABLED ?? "";
|
||||
const isGoogleConfigured = !!formattedConfig?.GOOGLE_CLIENT_ID && !!formattedConfig?.GOOGLE_CLIENT_SECRET;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isGoogleConfigured ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/authentication/google" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGoogleConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGoogleConfig = Boolean(parseInt(enableGoogleConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GOOGLE_ENABLED", newEnableGoogleConfig);
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
to="/authentication/google"
|
||||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
||||
>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
||||
Configure
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Mails, KeyRound } from "lucide-react";
|
||||
import { type TInstanceAuthenticationModes } from "@plane/types";
|
||||
import { type TGetBaseAuthenticationModeProps } from "@plane/types";
|
||||
import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch";
|
||||
import { PasswordLoginConfiguration } from "@/components/authentication/password-config-switch";
|
||||
import { GoogleConfiguration } from "@/components/authentication/google-config";
|
||||
import { GithubConfiguration } from "@/components/authentication/github-config";
|
||||
import { GitlabConfiguration } from "@/components/authentication/gitlab-config";
|
||||
// images
|
||||
import githubLightModeImage from "@/public/logos/github-black.png";
|
||||
import githubDarkModeImage from "@/public/logos/github-white.png";
|
||||
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
|
||||
import GoogleLogo from "@/public/logos/google-logo.svg";
|
||||
|
||||
export const getBaseAuthenticationModes: (props: TGetBaseAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({
|
||||
disabled,
|
||||
updateConfig,
|
||||
resolvedTheme,
|
||||
}) => [
|
||||
{
|
||||
key: "unique-codes",
|
||||
name: "Unique codes",
|
||||
description:
|
||||
"Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
|
||||
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
|
||||
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "passwords-login",
|
||||
name: "Passwords",
|
||||
description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.",
|
||||
icon: <KeyRound className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
|
||||
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "google",
|
||||
name: "Google",
|
||||
description: "Allow members to log in or sign up for Plane with their Google accounts.",
|
||||
icon: <img src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
|
||||
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "github",
|
||||
name: "GitHub",
|
||||
description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
|
||||
icon: (
|
||||
<img
|
||||
src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
|
||||
height={20}
|
||||
width={20}
|
||||
alt="GitHub Logo"
|
||||
/>
|
||||
),
|
||||
config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
key: "gitlab",
|
||||
name: "GitLab",
|
||||
description: "Allow members to log in or sign up to plane with their GitLab accounts.",
|
||||
icon: <img src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
|
||||
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
disabled: boolean;
|
||||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
export const PasswordLoginConfiguration: React.FC<Props> = observer((props) => {
|
||||
const { disabled, updateConfig } = props;
|
||||
// store
|
||||
const { formattedConfig } = useInstance();
|
||||
// derived values
|
||||
const enableEmailPassword = formattedConfig?.ENABLE_EMAIL_PASSWORD ?? "";
|
||||
|
||||
return (
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableEmailPassword))}
|
||||
onChange={() => {
|
||||
const newEnableEmailPassword = Boolean(parseInt(enableEmailPassword)) === true ? "0" : "1";
|
||||
updateConfig("ENABLE_EMAIL_PASSWORD", newEnableEmailPassword);
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { FC } from "react";
|
||||
import { AlertCircle, CheckCircle2 } from "lucide-react";
|
||||
|
||||
type TBanner = {
|
||||
type: "success" | "error";
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const Banner: FC<TBanner> = (props) => {
|
||||
const { type, message } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-md p-2 w-full border ${type === "error" ? "bg-red-500/5 border-red-400" : "bg-green-500/5 border-green-400"}`}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex-shrink-0">
|
||||
{type === "error" ? (
|
||||
<span className="flex items-center justify-center h-6 w-6 rounded-full">
|
||||
<AlertCircle className="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</span>
|
||||
) : (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-1">
|
||||
<p className={`text-sm font-medium ${type === "error" ? "text-red-600" : "text-green-600"}`}>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { Link } from "react-router";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
href?: string;
|
||||
icon?: React.ReactNode | undefined;
|
||||
};
|
||||
|
||||
export const BreadcrumbLink: React.FC<Props> = (props) => {
|
||||
const { href, label, icon } = props;
|
||||
return (
|
||||
<Tooltip tooltipContent={label} position="bottom">
|
||||
<li className="flex items-center space-x-2" tabIndex={-1}>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
{href ? (
|
||||
<Link
|
||||
className="flex items-center gap-1 text-sm font-medium text-custom-text-300 hover:text-custom-text-100"
|
||||
to={href}
|
||||
>
|
||||
{icon ? (
|
||||
<div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-[1rem]">{icon}</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="flex cursor-default items-center gap-1 text-sm font-medium text-custom-text-100">
|
||||
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden">{icon}</div>}
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type TProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
darkerShade?: boolean;
|
||||
};
|
||||
|
||||
export const CodeBlock = ({ children, className, darkerShade }: TProps) => (
|
||||
<span
|
||||
className={cn(
|
||||
"px-0.5 text-xs text-custom-text-300 bg-custom-background-90 font-semibold rounded-md border border-custom-border-100",
|
||||
{
|
||||
"text-custom-text-200 bg-custom-background-80 border-custom-border-200": darkerShade,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Link } from "react-router";
|
||||
// headless ui
|
||||
import { Dialog } from "@plane/propel/dialog";
|
||||
// ui
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
onDiscardHref: string;
|
||||
};
|
||||
|
||||
export const ConfirmDiscardModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, handleClose, onDiscardHref } = props;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-32">
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[30rem]">
|
||||
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<Dialog.Title className="text-lg font-medium leading-6 text-custom-text-300">
|
||||
You have unsaved changes
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-400">
|
||||
Changes you made will be lost if you go back. Do you wish to go back?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end items-center p-4 sm:px-6 gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Keep editing
|
||||
</Button>
|
||||
<Link to={onDiscardHref} className={getButtonStyling("primary", "sm")}>
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Controller, Control } from "react-hook-form";
|
||||
// icons
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { Input } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
control: Control<any>;
|
||||
type: "text" | "password";
|
||||
name: string;
|
||||
label: string;
|
||||
description?: string | React.ReactNode;
|
||||
placeholder: string;
|
||||
error: boolean;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
export type TControllerInputFormField = {
|
||||
key: string;
|
||||
type: "text" | "password";
|
||||
label: string;
|
||||
description?: string | React.ReactNode;
|
||||
placeholder: string;
|
||||
error: boolean;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
export const ControllerInput: React.FC<Props> = (props) => {
|
||||
const { name, control, type, label, description, placeholder, error, required } = props;
|
||||
// states
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">{label}</h4>
|
||||
<div className="relative">
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
rules={{ required: required ? `${label} is required.` : false }}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id={name}
|
||||
name={name}
|
||||
type={type === "password" && showPassword ? "text" : type}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={error}
|
||||
placeholder={placeholder}
|
||||
className={cn("w-full rounded-md font-medium", {
|
||||
"pr-10": type === "password",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{type === "password" &&
|
||||
(showPassword ? (
|
||||
<button
|
||||
tabIndex={-1}
|
||||
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
|
||||
onClick={() => setShowPassword(false)}
|
||||
>
|
||||
<EyeOff className="h-4 w-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
tabIndex={-1}
|
||||
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
|
||||
onClick={() => setShowPassword(true)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{description && <p className="pt-0.5 text-xs text-custom-text-300">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
// ui
|
||||
import { Copy } from "lucide-react";
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// icons
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
url: string;
|
||||
description: string | React.ReactNode;
|
||||
};
|
||||
|
||||
export type TCopyField = {
|
||||
key: string;
|
||||
label: string;
|
||||
url: string;
|
||||
description: string | React.ReactNode;
|
||||
};
|
||||
|
||||
export const CopyField: React.FC<Props> = (props) => {
|
||||
const { label, url, description } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-200">{label}</h4>
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
className="flex items-center justify-between py-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(url);
|
||||
setToast({
|
||||
type: TOAST_TYPE.INFO,
|
||||
title: "Copied to clipboard",
|
||||
message: `The ${label} has been successfully copied to your clipboard`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<p className="text-sm font-medium">{url}</p>
|
||||
<Copy size={18} color="#B9B9B9" />
|
||||
</Button>
|
||||
<div className="text-xs text-custom-text-300">{description}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Button } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: React.ReactNode;
|
||||
image?: any;
|
||||
primaryButton?: {
|
||||
icon?: any;
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
secondaryButton?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const EmptyState: React.FC<Props> = ({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
disabled = false,
|
||||
}) => (
|
||||
<div className={`flex h-full w-full items-center justify-center`}>
|
||||
<div className="flex w-full flex-col items-center text-center">
|
||||
{image && (
|
||||
<img
|
||||
src={image}
|
||||
className="w-52 sm:w-60"
|
||||
alt={primaryButton?.text || "button image"}
|
||||
/>
|
||||
)}
|
||||
<h6 className="mb-3 mt-6 text-xl font-semibold sm:mt-8">{title}</h6>
|
||||
{description && (
|
||||
<p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-4">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
variant="primary"
|
||||
prependIcon={primaryButton.icon}
|
||||
onClick={primaryButton.onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{primaryButton.text}
|
||||
</Button>
|
||||
)}
|
||||
{secondaryButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useTheme } from "next-themes";
|
||||
// assets
|
||||
import LogoSpinnerDark from "@/public/images/logo-spinner-dark.gif";
|
||||
import LogoSpinnerLight from "@/public/images/logo-spinner-light.gif";
|
||||
|
||||
export const LogoSpinner = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const logoSrc = resolvedTheme === "dark" ? LogoSpinnerLight : LogoSpinnerDark;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<img src={logoSrc} alt="logo" className="h-6 w-auto sm:h-11" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
type TPageHeader = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export const PageHeader: React.FC<TPageHeader> = (props) => {
|
||||
const { title = "God Mode - Plane", description = "Plane god mode" } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
import { type FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button } from "@plane/ui";
|
||||
// assets
|
||||
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
|
||||
import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg";
|
||||
import InstanceFailureImage from "@/public/instance/instance-failure.svg";
|
||||
|
||||
export const InstanceFailureView: FC = observer(() => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const instanceImage =
|
||||
resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
|
||||
|
||||
const handleRetry = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthHeader />
|
||||
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
|
||||
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<img src={instanceImage} alt="Plane Logo" />
|
||||
<h3 className="font-medium text-2xl text-white text-center">
|
||||
Unable to fetch instance details.
|
||||
</h3>
|
||||
<p className="font-medium text-base text-center">
|
||||
We were unable to fetch the details of the instance. Fret not, it
|
||||
might just be a connectivity issue.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button size="md" onClick={handleRetry}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
export const FormHeader = ({ heading, subHeading }: { heading: string; subHeading: string }) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-2xl font-semibold text-custom-text-100 leading-7">{heading}</span>
|
||||
<span className="text-lg font-semibold text-custom-text-400 leading-7">{subHeading}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { type FC } from "react";
|
||||
|
||||
import { Link } from "react-router";
|
||||
import { Button } from "@plane/ui";
|
||||
// assets
|
||||
import PlaneTakeOffImage from "@/public/images/plane-takeoff.png";
|
||||
|
||||
export const InstanceNotReady: FC = () => (
|
||||
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
|
||||
<div className="w-auto max-w-2xl relative space-y-8 py-10">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
|
||||
<img src={PlaneTakeOffImage} alt="Plane Logo" />
|
||||
<p className="font-medium text-base text-custom-text-400">
|
||||
Get started by setting up your instance and workspace
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Link to={"/setup/?auth_enabled=0"}>
|
||||
<Button size="lg" className="w-full">
|
||||
Get started
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useTheme } from "next-themes";
|
||||
// assets
|
||||
import LogoSpinnerDark from "@/public/images/logo-spinner-dark.gif";
|
||||
import LogoSpinnerLight from "@/public/images/logo-spinner-light.gif";
|
||||
|
||||
export const InstanceLoading = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const logoSrc = resolvedTheme === "dark" ? LogoSpinnerLight : LogoSpinnerDark;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<img src={logoSrc} alt="logo" className="h-6 w-auto sm:h-11" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,353 @@
|
||||
"use client";
|
||||
|
||||
import { type FC, useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
// icons
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Button, Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
|
||||
import { getPasswordStrength } from "@plane/utils";
|
||||
// components
|
||||
import { Banner } from "@/components/common/banner";
|
||||
import { FormHeader } from "@/components/instance/form-header";
|
||||
import { AuthHeader } from "@/components/instance/sign-in/header";
|
||||
|
||||
// service initialization
|
||||
const authService = new AuthService();
|
||||
|
||||
// error codes
|
||||
enum EErrorCodes {
|
||||
INSTANCE_NOT_CONFIGURED = "INSTANCE_NOT_CONFIGURED",
|
||||
ADMIN_ALREADY_EXIST = "ADMIN_ALREADY_EXIST",
|
||||
REQUIRED_EMAIL_PASSWORD_FIRST_NAME = "REQUIRED_EMAIL_PASSWORD_FIRST_NAME",
|
||||
INVALID_EMAIL = "INVALID_EMAIL",
|
||||
INVALID_PASSWORD = "INVALID_PASSWORD",
|
||||
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS",
|
||||
}
|
||||
|
||||
type TError = {
|
||||
type: EErrorCodes | undefined;
|
||||
message: string | undefined;
|
||||
};
|
||||
|
||||
// form data
|
||||
type TFormData = {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
company_name: string;
|
||||
password: string;
|
||||
confirm_password?: string;
|
||||
is_telemetry_enabled: boolean;
|
||||
};
|
||||
|
||||
const defaultFromData: TFormData = {
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
email: "",
|
||||
company_name: "",
|
||||
password: "",
|
||||
is_telemetry_enabled: true,
|
||||
};
|
||||
|
||||
export const InstanceSetupForm: FC = (props) => {
|
||||
const {} = props;
|
||||
// search params
|
||||
const [searchParams] = useSearchParams();
|
||||
const firstNameParam = searchParams.get("first_name") || undefined;
|
||||
const lastNameParam = searchParams.get("last_name") || undefined;
|
||||
const companyParam = searchParams.get("company") || undefined;
|
||||
const emailParam = searchParams.get("email") || undefined;
|
||||
const isTelemetryEnabledParam = (searchParams.get("is_telemetry_enabled") === "True" ? true : false) || true;
|
||||
const errorCode = searchParams.get("error_code") || undefined;
|
||||
const errorMessage = searchParams.get("error_message") || undefined;
|
||||
// state
|
||||
const [showPassword, setShowPassword] = useState({
|
||||
password: false,
|
||||
retypePassword: false,
|
||||
});
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
const [formData, setFormData] = useState<TFormData>(defaultFromData);
|
||||
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false);
|
||||
|
||||
const handleShowPassword = (key: keyof typeof showPassword) =>
|
||||
setShowPassword((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||
|
||||
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
|
||||
setFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
useEffect(() => {
|
||||
if (csrfToken === undefined)
|
||||
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||
}, [csrfToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (firstNameParam) setFormData((prev) => ({ ...prev, first_name: firstNameParam }));
|
||||
if (lastNameParam) setFormData((prev) => ({ ...prev, last_name: lastNameParam }));
|
||||
if (companyParam) setFormData((prev) => ({ ...prev, company_name: companyParam }));
|
||||
if (emailParam) setFormData((prev) => ({ ...prev, email: emailParam }));
|
||||
if (isTelemetryEnabledParam) setFormData((prev) => ({ ...prev, is_telemetry_enabled: isTelemetryEnabledParam }));
|
||||
}, [firstNameParam, lastNameParam, companyParam, emailParam, isTelemetryEnabledParam]);
|
||||
|
||||
// derived values
|
||||
const errorData: TError = useMemo(() => {
|
||||
if (errorCode && errorMessage) {
|
||||
switch (errorCode) {
|
||||
case EErrorCodes.INSTANCE_NOT_CONFIGURED:
|
||||
return { type: EErrorCodes.INSTANCE_NOT_CONFIGURED, message: errorMessage };
|
||||
case EErrorCodes.ADMIN_ALREADY_EXIST:
|
||||
return { type: EErrorCodes.ADMIN_ALREADY_EXIST, message: errorMessage };
|
||||
case EErrorCodes.REQUIRED_EMAIL_PASSWORD_FIRST_NAME:
|
||||
return { type: EErrorCodes.REQUIRED_EMAIL_PASSWORD_FIRST_NAME, message: errorMessage };
|
||||
case EErrorCodes.INVALID_EMAIL:
|
||||
return { type: EErrorCodes.INVALID_EMAIL, message: errorMessage };
|
||||
case EErrorCodes.INVALID_PASSWORD:
|
||||
return { type: EErrorCodes.INVALID_PASSWORD, message: errorMessage };
|
||||
case EErrorCodes.USER_ALREADY_EXISTS:
|
||||
return { type: EErrorCodes.USER_ALREADY_EXISTS, message: errorMessage };
|
||||
default:
|
||||
return { type: undefined, message: undefined };
|
||||
}
|
||||
} else return { type: undefined, message: undefined };
|
||||
}, [errorCode, errorMessage]);
|
||||
|
||||
const isButtonDisabled = useMemo(
|
||||
() =>
|
||||
!isSubmitting &&
|
||||
formData.first_name &&
|
||||
formData.email &&
|
||||
formData.password &&
|
||||
getPasswordStrength(formData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID &&
|
||||
formData.password === formData.confirm_password
|
||||
? false
|
||||
: true,
|
||||
[formData.confirm_password, formData.email, formData.first_name, formData.password, isSubmitting]
|
||||
);
|
||||
|
||||
const password = formData?.password ?? "";
|
||||
const confirmPassword = formData?.confirm_password ?? "";
|
||||
const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthHeader />
|
||||
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
|
||||
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
|
||||
<FormHeader
|
||||
heading="Setup your Plane Instance"
|
||||
subHeading="Post setup you will be able to manage this Plane instance."
|
||||
/>
|
||||
{errorData.type &&
|
||||
errorData?.message &&
|
||||
![EErrorCodes.INVALID_EMAIL, EErrorCodes.INVALID_PASSWORD].includes(errorData.type) && (
|
||||
<Banner type="error" message={errorData?.message} />
|
||||
)}
|
||||
<form
|
||||
className="space-y-4"
|
||||
method="POST"
|
||||
action={`${API_BASE_URL}/api/instances/admins/sign-up/`}
|
||||
onSubmit={() => setIsSubmitting(true)}
|
||||
onError={() => setIsSubmitting(false)}
|
||||
>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<input type="hidden" name="is_telemetry_enabled" value={formData.is_telemetry_enabled ? "True" : "False"} />
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center gap-4">
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="first_name">
|
||||
First name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
id="first_name"
|
||||
name="first_name"
|
||||
type="text"
|
||||
inputSize="md"
|
||||
placeholder="Wilber"
|
||||
value={formData.first_name}
|
||||
onChange={(e) => handleFormChange("first_name", e.target.value)}
|
||||
autoComplete="on"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="last_name">
|
||||
Last name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
id="last_name"
|
||||
name="last_name"
|
||||
type="text"
|
||||
inputSize="md"
|
||||
placeholder="Wright"
|
||||
value={formData.last_name}
|
||||
onChange={(e) => handleFormChange("last_name", e.target.value)}
|
||||
autoComplete="on"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
|
||||
Email <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
inputSize="md"
|
||||
placeholder="name@company.com"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL ? true : false}
|
||||
autoComplete="on"
|
||||
/>
|
||||
{errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (
|
||||
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="company_name">
|
||||
Company name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
id="company_name"
|
||||
name="company_name"
|
||||
type="text"
|
||||
inputSize="md"
|
||||
placeholder="Company name"
|
||||
value={formData.company_name}
|
||||
onChange={(e) => handleFormChange("company_name", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
|
||||
Set a password <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword.password ? "text" : "password"}
|
||||
inputSize="md"
|
||||
placeholder="New password..."
|
||||
value={formData.password}
|
||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
|
||||
onFocus={() => setIsPasswordInputFocused(true)}
|
||||
onBlur={() => setIsPasswordInputFocused(false)}
|
||||
autoComplete="on"
|
||||
/>
|
||||
{showPassword.password ? (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||
onClick={() => handleShowPassword("password")}
|
||||
>
|
||||
<EyeOff className="h-4 w-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||
onClick={() => handleShowPassword("password")}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && (
|
||||
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
|
||||
)}
|
||||
<PasswordStrengthIndicator password={formData.password} isFocused={isPasswordInputFocused} />
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="confirm_password">
|
||||
Confirm password <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword.retypePassword ? "text" : "password"}
|
||||
id="confirm_password"
|
||||
name="confirm_password"
|
||||
inputSize="md"
|
||||
value={formData.confirm_password}
|
||||
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
|
||||
placeholder="Confirm password"
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
|
||||
onFocus={() => setIsRetryPasswordInputFocused(true)}
|
||||
onBlur={() => setIsRetryPasswordInputFocused(false)}
|
||||
/>
|
||||
{showPassword.retypePassword ? (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||
onClick={() => handleShowPassword("retypePassword")}
|
||||
>
|
||||
<EyeOff className="h-4 w-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||
onClick={() => handleShowPassword("retypePassword")}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{!!formData.confirm_password &&
|
||||
formData.password !== formData.confirm_password &&
|
||||
renderPasswordMatchError && <span className="text-sm text-red-500">Passwords don{"'"}t match</span>}
|
||||
</div>
|
||||
|
||||
<div className="relative flex gap-2">
|
||||
<div>
|
||||
<Checkbox
|
||||
className="w-4 h-4"
|
||||
iconClassName="w-3 h-3"
|
||||
id="is_telemetry_enabled"
|
||||
onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
|
||||
checked={formData.is_telemetry_enabled}
|
||||
/>
|
||||
</div>
|
||||
<label className="text-sm text-custom-text-300 font-medium cursor-pointer" htmlFor="is_telemetry_enabled">
|
||||
Allow Plane to anonymously collect usage events.{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://developers.plane.so/self-hosting/telemetry"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm font-medium text-blue-500 hover:text-blue-600 flex-shrink-0"
|
||||
>
|
||||
See More
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="py-2">
|
||||
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
|
||||
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Info, X } from "lucide-react";
|
||||
// plane constants
|
||||
import type { TAdminAuthErrorInfo } from "@plane/constants";
|
||||
|
||||
type TAuthBanner = {
|
||||
bannerData: TAdminAuthErrorInfo | undefined;
|
||||
handleBannerData?: (bannerData: TAdminAuthErrorInfo | undefined) => void;
|
||||
};
|
||||
|
||||
export const AuthBanner = (props: TAuthBanner) => {
|
||||
const { bannerData, handleBannerData } = props;
|
||||
|
||||
if (!bannerData) return <></>;
|
||||
return (
|
||||
<div className="relative flex items-center p-2 rounded-md gap-2 border border-custom-primary-100/50 bg-custom-primary-100/10">
|
||||
<div className="w-4 h-4 flex-shrink-0 relative flex justify-center items-center">
|
||||
<Info size={16} className="text-custom-primary-100" />
|
||||
</div>
|
||||
<div className="w-full text-sm font-medium text-custom-primary-100">
|
||||
{bannerData?.message}
|
||||
</div>
|
||||
<div
|
||||
className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/80"
|
||||
onClick={() => handleBannerData && handleBannerData(undefined)}
|
||||
>
|
||||
<X className="w-4 h-4 flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,205 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL, EAdminAuthErrorCodes, type TAdminAuthErrorInfo } from "@plane/constants";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Button, Input, Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { Banner } from "@/components/common/banner";
|
||||
// local components
|
||||
import { FormHeader } from "../form-header";
|
||||
import { AuthBanner } from "./banner";
|
||||
import { AuthHeader } from "./header";
|
||||
import { authErrorHandler } from "./helpers";
|
||||
|
||||
// service initialization
|
||||
const authService = new AuthService();
|
||||
|
||||
// error codes
|
||||
enum EErrorCodes {
|
||||
INSTANCE_NOT_CONFIGURED = "INSTANCE_NOT_CONFIGURED",
|
||||
REQUIRED_EMAIL_PASSWORD = "REQUIRED_EMAIL_PASSWORD",
|
||||
INVALID_EMAIL = "INVALID_EMAIL",
|
||||
USER_DOES_NOT_EXIST = "USER_DOES_NOT_EXIST",
|
||||
AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED",
|
||||
}
|
||||
|
||||
type TError = {
|
||||
type: EErrorCodes | undefined;
|
||||
message: string | undefined;
|
||||
};
|
||||
|
||||
// form data
|
||||
type TFormData = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
const defaultFromData: TFormData = {
|
||||
email: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
export const InstanceSignInForm = () => {
|
||||
// search params
|
||||
const [searchParams] = useSearchParams();
|
||||
const emailParam = searchParams.get("email") || undefined;
|
||||
const errorCode = searchParams.get("error_code") || undefined;
|
||||
const errorMessage = searchParams.get("error_message") || undefined;
|
||||
// state
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
const [formData, setFormData] = useState<TFormData>(defaultFromData);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [errorInfo, setErrorInfo] = useState<TAdminAuthErrorInfo | undefined>(undefined);
|
||||
|
||||
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
|
||||
setFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
useEffect(() => {
|
||||
if (csrfToken === undefined)
|
||||
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||
}, [csrfToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (emailParam) setFormData((prev) => ({ ...prev, email: emailParam }));
|
||||
}, [emailParam]);
|
||||
|
||||
// derived values
|
||||
const errorData: TError = useMemo(() => {
|
||||
if (errorCode && errorMessage) {
|
||||
switch (errorCode) {
|
||||
case EErrorCodes.INSTANCE_NOT_CONFIGURED:
|
||||
return {
|
||||
type: EErrorCodes.INSTANCE_NOT_CONFIGURED,
|
||||
message: errorMessage,
|
||||
};
|
||||
case EErrorCodes.REQUIRED_EMAIL_PASSWORD:
|
||||
return {
|
||||
type: EErrorCodes.REQUIRED_EMAIL_PASSWORD,
|
||||
message: errorMessage,
|
||||
};
|
||||
case EErrorCodes.INVALID_EMAIL:
|
||||
return { type: EErrorCodes.INVALID_EMAIL, message: errorMessage };
|
||||
case EErrorCodes.USER_DOES_NOT_EXIST:
|
||||
return {
|
||||
type: EErrorCodes.USER_DOES_NOT_EXIST,
|
||||
message: errorMessage,
|
||||
};
|
||||
case EErrorCodes.AUTHENTICATION_FAILED:
|
||||
return {
|
||||
type: EErrorCodes.AUTHENTICATION_FAILED,
|
||||
message: errorMessage,
|
||||
};
|
||||
default:
|
||||
return { type: undefined, message: undefined };
|
||||
}
|
||||
} else return { type: undefined, message: undefined };
|
||||
}, [errorCode, errorMessage]);
|
||||
|
||||
const isButtonDisabled = useMemo(
|
||||
() => (!isSubmitting && formData.email && formData.password ? false : true),
|
||||
[formData.email, formData.password, isSubmitting]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (errorCode) {
|
||||
const errorDetail = authErrorHandler(errorCode?.toString() as EAdminAuthErrorCodes);
|
||||
if (errorDetail) {
|
||||
setErrorInfo(errorDetail);
|
||||
}
|
||||
}
|
||||
}, [errorCode]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthHeader />
|
||||
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
|
||||
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
|
||||
<FormHeader
|
||||
heading="Manage your Plane instance"
|
||||
subHeading="Configure instance-wide settings to secure your instance"
|
||||
/>
|
||||
<form
|
||||
className="space-y-4"
|
||||
method="POST"
|
||||
action={`${API_BASE_URL}/api/instances/admins/sign-in/`}
|
||||
onSubmit={() => setIsSubmitting(true)}
|
||||
onError={() => setIsSubmitting(false)}
|
||||
>
|
||||
{errorData.type && errorData?.message ? (
|
||||
<Banner type="error" message={errorData?.message} />
|
||||
) : (
|
||||
<>
|
||||
{errorInfo && <AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />}
|
||||
</>
|
||||
)}
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
|
||||
Email <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
inputSize="md"
|
||||
placeholder="name@company.com"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
autoComplete="on"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
|
||||
Password <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
inputSize="md"
|
||||
placeholder="Enter your password"
|
||||
value={formData.password}
|
||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||
autoComplete="on"
|
||||
/>
|
||||
{showPassword ? (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||
onClick={() => setShowPassword(false)}
|
||||
>
|
||||
<EyeOff className="h-4 w-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||
onClick={() => setShowPassword(true)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
|
||||
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Sign in"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { Link } from "react-router";
|
||||
import { PlaneLockup } from "@plane/propel/icons";
|
||||
|
||||
export const AuthHeader = () => (
|
||||
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
|
||||
<Link to="/">
|
||||
<PlaneLockup height={20} width={95} className="text-custom-text-100" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,100 @@
|
||||
import { type ReactNode } from "react";
|
||||
import { Link } from "react-router";
|
||||
// plane packages
|
||||
import { SUPPORT_EMAIL, EAdminAuthErrorCodes, type TAdminAuthErrorInfo } from "@plane/constants";
|
||||
|
||||
export enum EErrorAlertType {
|
||||
BANNER_ALERT = "BANNER_ALERT",
|
||||
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
|
||||
INLINE_EMAIL = "INLINE_EMAIL",
|
||||
INLINE_PASSWORD = "INLINE_PASSWORD",
|
||||
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
|
||||
}
|
||||
|
||||
const errorCodeMessages: {
|
||||
[key in EAdminAuthErrorCodes]: {
|
||||
title: string;
|
||||
message: (email?: string | undefined) => ReactNode;
|
||||
};
|
||||
} = {
|
||||
// admin
|
||||
[EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST]: {
|
||||
title: `Admin already exists`,
|
||||
message: () => `Admin already exists. Please try again.`,
|
||||
},
|
||||
[EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
|
||||
title: `Email, password and first name required`,
|
||||
message: () => `Email, password and first name required. Please try again.`,
|
||||
},
|
||||
[EAdminAuthErrorCodes.INVALID_ADMIN_EMAIL]: {
|
||||
title: `Invalid admin email`,
|
||||
message: () => `Invalid admin email. Please try again.`,
|
||||
},
|
||||
[EAdminAuthErrorCodes.INVALID_ADMIN_PASSWORD]: {
|
||||
title: `Invalid admin password`,
|
||||
message: () => `Invalid admin password. Please try again.`,
|
||||
},
|
||||
[EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
|
||||
title: `Email and password required`,
|
||||
message: () => `Email and password required. Please try again.`,
|
||||
},
|
||||
[EAdminAuthErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
|
||||
title: `Authentication failed`,
|
||||
message: () => `Authentication failed. Please try again.`,
|
||||
},
|
||||
[EAdminAuthErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
|
||||
title: `Admin user already exists`,
|
||||
message: () => (
|
||||
<div>
|
||||
Admin user already exists.
|
||||
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" to="/">
|
||||
Sign In
|
||||
</Link>
|
||||
now.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EAdminAuthErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
|
||||
title: `Admin user does not exist`,
|
||||
message: () => (
|
||||
<div>
|
||||
Admin user does not exist.
|
||||
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" to="/">
|
||||
Sign In
|
||||
</Link>
|
||||
now.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EAdminAuthErrorCodes.ADMIN_USER_DEACTIVATED]: {
|
||||
title: `User account deactivated`,
|
||||
message: () => `User account deactivated. Please contact ${!!SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
|
||||
},
|
||||
};
|
||||
|
||||
export const authErrorHandler = (
|
||||
errorCode: EAdminAuthErrorCodes,
|
||||
email?: string | undefined
|
||||
): TAdminAuthErrorInfo | undefined => {
|
||||
const bannerAlertErrorCodes = [
|
||||
EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST,
|
||||
EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
|
||||
EAdminAuthErrorCodes.INVALID_ADMIN_EMAIL,
|
||||
EAdminAuthErrorCodes.INVALID_ADMIN_PASSWORD,
|
||||
EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
|
||||
EAdminAuthErrorCodes.ADMIN_AUTHENTICATION_FAILED,
|
||||
EAdminAuthErrorCodes.ADMIN_USER_ALREADY_EXIST,
|
||||
EAdminAuthErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
|
||||
EAdminAuthErrorCodes.ADMIN_USER_DEACTIVATED,
|
||||
];
|
||||
|
||||
if (bannerAlertErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.BANNER_ALERT,
|
||||
code: errorCode,
|
||||
title: errorCodeMessages[errorCode]?.title || "Error",
|
||||
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Link } from "react-router";
|
||||
import { useTheme as nextUseTheme } from "next-themes";
|
||||
// ui
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
// icons
|
||||
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
|
||||
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
|
||||
|
||||
export const NewUserPopup: React.FC = observer(() => {
|
||||
// hooks
|
||||
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
|
||||
// theme
|
||||
const { resolvedTheme } = nextUseTheme();
|
||||
|
||||
if (!isNewUserPopup) return <></>;
|
||||
return (
|
||||
<div className="absolute bottom-8 right-8 p-6 w-96 border border-custom-border-100 shadow-md rounded-lg bg-custom-background-100">
|
||||
<div className="flex gap-4">
|
||||
<div className="grow">
|
||||
<div className="text-base font-semibold">Create workspace</div>
|
||||
<div className="py-2 text-sm font-medium text-custom-text-300">
|
||||
Instance setup done! Welcome to Plane instance portal. Start your journey with by creating your first
|
||||
workspace.
|
||||
</div>
|
||||
<div className="flex items-center gap-4 pt-2">
|
||||
<Link to="/workspace/create" className={getButtonStyling("primary", "sm")}>
|
||||
Create workspace
|
||||
</Link>
|
||||
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 flex items-center justify-center">
|
||||
<img
|
||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? TakeoffIconDark : TakeoffIconLight}
|
||||
height={80}
|
||||
width={80}
|
||||
alt="Plane icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { WEB_BASE_URL } from "@plane/constants";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
||||
type TWorkspaceListItemProps = {
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const WorkspaceListItem = observer(({ workspaceId }: TWorkspaceListItemProps) => {
|
||||
// store hooks
|
||||
const { getWorkspaceById } = useWorkspace();
|
||||
// derived values
|
||||
const workspace = getWorkspaceById(workspaceId);
|
||||
|
||||
if (!workspace) return null;
|
||||
return (
|
||||
<a
|
||||
key={workspaceId}
|
||||
href={`${WEB_BASE_URL}/${encodeURIComponent(workspace.slug)}`}
|
||||
target="_blank"
|
||||
className="group flex items-center justify-between p-4 gap-2.5 truncate border border-custom-border-200/70 hover:border-custom-border-200 hover:bg-custom-background-90 rounded-md"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<span
|
||||
className={`relative flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 mt-1 text-xs uppercase ${
|
||||
!workspace?.logo_url && "rounded bg-custom-primary-500 text-white"
|
||||
}`}
|
||||
>
|
||||
{workspace?.logo_url && workspace.logo_url !== "" ? (
|
||||
<img
|
||||
src={getFileURL(workspace.logo_url)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded object-cover"
|
||||
alt="Workspace Logo"
|
||||
/>
|
||||
) : (
|
||||
(workspace?.name?.[0] ?? "...")
|
||||
)}
|
||||
</span>
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<div className="flex flex-wrap w-full items-center gap-2.5">
|
||||
<h3 className={`text-base font-medium capitalize`}>{workspace.name}</h3>/
|
||||
<Tooltip tooltipContent="The unique URL of your workspace">
|
||||
<h4 className="text-sm text-custom-text-300">[{workspace.slug}]</h4>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{workspace.owner.email && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<h3 className="text-custom-text-200 font-medium">Owned by:</h3>
|
||||
<h4 className="text-custom-text-300">{workspace.owner.email}</h4>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
{workspace.total_projects !== null && (
|
||||
<span className="flex items-center gap-1">
|
||||
<h3 className="text-custom-text-200 font-medium">Total projects:</h3>
|
||||
<h4 className="text-custom-text-300">{workspace.total_projects}</h4>
|
||||
</span>
|
||||
)}
|
||||
{workspace.total_members !== null && (
|
||||
<>
|
||||
•
|
||||
<span className="flex items-center gap-1">
|
||||
<h3 className="text-custom-text-200 font-medium">Total members:</h3>
|
||||
<h4 className="text-custom-text-300">{workspace.total_members}</h4>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<ExternalLink size={14} className="text-custom-text-400 group-hover:text-custom-text-200" />
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./use-theme";
|
||||
export * from "./use-instance";
|
||||
export * from "./use-user";
|
||||
export * from "./use-workspace";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/app/store.provider";
|
||||
import type { IInstanceStore } from "@/store/instance.store";
|
||||
|
||||
export const useInstance = (): IInstanceStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useInstance must be used within StoreProvider");
|
||||
return context.instance;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/app/store.provider";
|
||||
import type { IThemeStore } from "@/store/theme.store";
|
||||
|
||||
export const useTheme = (): IThemeStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useTheme must be used within StoreProvider");
|
||||
return context.theme;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/app/store.provider";
|
||||
import { type IUserStore } from "@/store/user.store";
|
||||
|
||||
export const useUser = (): IUserStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useUser must be used within StoreProvider");
|
||||
return context.user;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/app/store.provider";
|
||||
import type { IWorkspaceStore } from "@/store/workspace.store";
|
||||
|
||||
export const useWorkspace = (): IWorkspaceStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useWorkspace must be used within StoreProvider");
|
||||
return context.workspace;
|
||||
};
|
||||
@@ -0,0 +1,217 @@
|
||||
import set from "lodash/set";
|
||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||
// plane internal packages
|
||||
import { EInstanceStatus, type TInstanceStatus } from "@plane/constants";
|
||||
import { InstanceService } from "@plane/services";
|
||||
import type {
|
||||
IInstance,
|
||||
IInstanceAdmin,
|
||||
IInstanceConfiguration,
|
||||
IFormattedInstanceConfiguration,
|
||||
IInstanceInfo,
|
||||
IInstanceConfig,
|
||||
} from "@plane/types";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
export interface IInstanceStore {
|
||||
// issues
|
||||
isLoading: boolean;
|
||||
error: any;
|
||||
instanceStatus: TInstanceStatus | undefined;
|
||||
instance: IInstance | undefined;
|
||||
config: IInstanceConfig | undefined;
|
||||
instanceAdmins: IInstanceAdmin[] | undefined;
|
||||
instanceConfigurations: IInstanceConfiguration[] | undefined;
|
||||
// computed
|
||||
formattedConfig: IFormattedInstanceConfiguration | undefined;
|
||||
// action
|
||||
hydrate: (data: IInstanceInfo) => void;
|
||||
fetchInstanceInfo: () => Promise<IInstanceInfo | undefined>;
|
||||
updateInstanceInfo: (data: Partial<IInstance>) => Promise<IInstance | undefined>;
|
||||
fetchInstanceAdmins: () => Promise<IInstanceAdmin[] | undefined>;
|
||||
fetchInstanceConfigurations: () => Promise<IInstanceConfiguration[] | undefined>;
|
||||
updateInstanceConfigurations: (data: Partial<IFormattedInstanceConfiguration>) => Promise<IInstanceConfiguration[]>;
|
||||
disableEmail: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class InstanceStore implements IInstanceStore {
|
||||
isLoading: boolean = true;
|
||||
error: any = undefined;
|
||||
instanceStatus: TInstanceStatus | undefined = undefined;
|
||||
instance: IInstance | undefined = undefined;
|
||||
config: IInstanceConfig | undefined = undefined;
|
||||
instanceAdmins: IInstanceAdmin[] | undefined = undefined;
|
||||
instanceConfigurations: IInstanceConfiguration[] | undefined = undefined;
|
||||
// service
|
||||
instanceService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
isLoading: observable.ref,
|
||||
error: observable.ref,
|
||||
instanceStatus: observable,
|
||||
instance: observable,
|
||||
instanceAdmins: observable,
|
||||
instanceConfigurations: observable,
|
||||
// computed
|
||||
formattedConfig: computed,
|
||||
// actions
|
||||
hydrate: action,
|
||||
fetchInstanceInfo: action,
|
||||
fetchInstanceAdmins: action,
|
||||
updateInstanceInfo: action,
|
||||
fetchInstanceConfigurations: action,
|
||||
updateInstanceConfigurations: action,
|
||||
});
|
||||
|
||||
this.instanceService = new InstanceService();
|
||||
}
|
||||
|
||||
hydrate = (data: IInstanceInfo) => {
|
||||
if (data) {
|
||||
this.instance = data.instance;
|
||||
this.config = data.config;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* computed value for instance configurations data for forms.
|
||||
* @returns configurations in the form of {key, value} pair.
|
||||
*/
|
||||
get formattedConfig() {
|
||||
if (!this.instanceConfigurations) return undefined;
|
||||
return this.instanceConfigurations?.reduce((formData: IFormattedInstanceConfiguration, config) => {
|
||||
formData[config.key] = config.value;
|
||||
return formData;
|
||||
}, {} as IFormattedInstanceConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description fetching instance configuration
|
||||
* @returns {IInstance} instance
|
||||
*/
|
||||
fetchInstanceInfo = async () => {
|
||||
try {
|
||||
if (this.instance === undefined) this.isLoading = true;
|
||||
this.error = undefined;
|
||||
const instanceInfo = await this.instanceService.info();
|
||||
// handling the new user popup toggle
|
||||
if (this.instance === undefined && !instanceInfo?.instance?.workspaces_exist)
|
||||
this.store.theme.toggleNewUserPopup();
|
||||
runInAction(() => {
|
||||
// console.log("instanceInfo: ", instanceInfo);
|
||||
this.isLoading = false;
|
||||
this.instance = instanceInfo.instance;
|
||||
this.config = instanceInfo.config;
|
||||
});
|
||||
return instanceInfo;
|
||||
} catch (error) {
|
||||
console.error("Error fetching the instance info");
|
||||
this.isLoading = false;
|
||||
this.error = { message: "Failed to fetch the instance info" };
|
||||
this.instanceStatus = {
|
||||
status: EInstanceStatus.ERROR,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description updating instance information
|
||||
* @param {Partial<IInstance>} data
|
||||
* @returns void
|
||||
*/
|
||||
updateInstanceInfo = async (data: Partial<IInstance>) => {
|
||||
try {
|
||||
const instanceResponse = await this.instanceService.update(data);
|
||||
if (instanceResponse) {
|
||||
runInAction(() => {
|
||||
if (this.instance) set(this.instance, "instance", instanceResponse);
|
||||
});
|
||||
}
|
||||
return instanceResponse;
|
||||
} catch (error) {
|
||||
console.error("Error updating the instance info");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description fetching instance admins
|
||||
* @return {IInstanceAdmin[]} instanceAdmins
|
||||
*/
|
||||
fetchInstanceAdmins = async () => {
|
||||
try {
|
||||
const instanceAdmins = await this.instanceService.admins();
|
||||
if (instanceAdmins) runInAction(() => (this.instanceAdmins = instanceAdmins));
|
||||
return instanceAdmins;
|
||||
} catch (error) {
|
||||
console.error("Error fetching the instance admins");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description fetching instance configurations
|
||||
* @return {IInstanceAdmin[]} instanceConfigurations
|
||||
*/
|
||||
fetchInstanceConfigurations = async () => {
|
||||
try {
|
||||
const instanceConfigurations = await this.instanceService.configurations();
|
||||
if (instanceConfigurations) runInAction(() => (this.instanceConfigurations = instanceConfigurations));
|
||||
return instanceConfigurations;
|
||||
} catch (error) {
|
||||
console.error("Error fetching the instance configurations");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description updating instance configurations
|
||||
* @param data
|
||||
*/
|
||||
updateInstanceConfigurations = async (data: Partial<IFormattedInstanceConfiguration>) => {
|
||||
try {
|
||||
const response = await this.instanceService.updateConfigurations(data);
|
||||
runInAction(() => {
|
||||
this.instanceConfigurations = this.instanceConfigurations?.map((config) => {
|
||||
const item = response.find((item) => item.key === config.key);
|
||||
if (item) return item;
|
||||
return config;
|
||||
});
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error updating the instance configurations");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
disableEmail = async () => {
|
||||
const instanceConfigurations = this.instanceConfigurations;
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.instanceConfigurations = this.instanceConfigurations?.map((config) => {
|
||||
if (
|
||||
[
|
||||
"EMAIL_HOST",
|
||||
"EMAIL_PORT",
|
||||
"EMAIL_HOST_USER",
|
||||
"EMAIL_HOST_PASSWORD",
|
||||
"EMAIL_FROM",
|
||||
"ENABLE_SMTP",
|
||||
].includes(config.key)
|
||||
)
|
||||
return { ...config, value: "" };
|
||||
return config;
|
||||
});
|
||||
});
|
||||
await this.instanceService.disableEmail();
|
||||
} catch (_error) {
|
||||
console.error("Error disabling the email");
|
||||
this.instanceConfigurations = instanceConfigurations;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { enableStaticRendering } from "mobx-react";
|
||||
// stores
|
||||
import { type IInstanceStore, InstanceStore } from "./instance.store";
|
||||
import { type IThemeStore, ThemeStore } from "./theme.store";
|
||||
import { type IUserStore, UserStore } from "./user.store";
|
||||
import { type IWorkspaceStore, WorkspaceStore } from "./workspace.store";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
export abstract class CoreRootStore {
|
||||
theme: IThemeStore;
|
||||
instance: IInstanceStore;
|
||||
user: IUserStore;
|
||||
workspace: IWorkspaceStore;
|
||||
|
||||
constructor() {
|
||||
this.theme = new ThemeStore(this);
|
||||
this.instance = new InstanceStore(this);
|
||||
this.user = new UserStore(this);
|
||||
this.workspace = new WorkspaceStore(this);
|
||||
}
|
||||
|
||||
hydrate(initialData: any) {
|
||||
this.theme.hydrate(initialData.theme);
|
||||
this.instance.hydrate(initialData.instance);
|
||||
this.user.hydrate(initialData.user);
|
||||
this.workspace.hydrate(initialData.workspace);
|
||||
}
|
||||
|
||||
resetOnSignOut() {
|
||||
// Only access localStorage on client side to prevent hydration issues
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("theme", "system");
|
||||
}
|
||||
this.instance = new InstanceStore(this);
|
||||
this.user = new UserStore(this);
|
||||
this.theme = new ThemeStore(this);
|
||||
this.workspace = new WorkspaceStore(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { action, observable, makeObservable } from "mobx";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
type TTheme = "dark" | "light";
|
||||
export interface IThemeStore {
|
||||
// observables
|
||||
isNewUserPopup: boolean;
|
||||
theme: string | undefined;
|
||||
isSidebarCollapsed: boolean | undefined;
|
||||
// actions
|
||||
hydrate: (data: any) => void;
|
||||
toggleNewUserPopup: () => void;
|
||||
toggleSidebar: (collapsed: boolean) => void;
|
||||
setTheme: (currentTheme: TTheme) => void;
|
||||
}
|
||||
|
||||
export class ThemeStore implements IThemeStore {
|
||||
// observables
|
||||
isNewUserPopup: boolean = false;
|
||||
isSidebarCollapsed: boolean | undefined = undefined;
|
||||
theme: string | undefined = undefined;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isNewUserPopup: observable.ref,
|
||||
isSidebarCollapsed: observable.ref,
|
||||
theme: observable.ref,
|
||||
// action
|
||||
toggleNewUserPopup: action,
|
||||
toggleSidebar: action,
|
||||
setTheme: action,
|
||||
});
|
||||
}
|
||||
|
||||
hydrate = (data: any) => {
|
||||
if (data) this.theme = data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Toggle the new user popup modal
|
||||
*/
|
||||
toggleNewUserPopup = () => (this.isNewUserPopup = !this.isNewUserPopup);
|
||||
|
||||
/**
|
||||
* @description Toggle the sidebar collapsed state
|
||||
* @param isCollapsed
|
||||
*/
|
||||
toggleSidebar = (isCollapsed: boolean) => {
|
||||
if (isCollapsed === undefined) this.isSidebarCollapsed = !this.isSidebarCollapsed;
|
||||
else this.isSidebarCollapsed = isCollapsed;
|
||||
// Only access localStorage on client side to prevent hydration issues
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("god_mode_sidebar_collapsed", isCollapsed.toString());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Sets the user theme and applies it to the platform
|
||||
* @param currentTheme
|
||||
*/
|
||||
setTheme = async (currentTheme: TTheme) => {
|
||||
try {
|
||||
// Only access localStorage on client side to prevent hydration issues
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("theme", currentTheme);
|
||||
}
|
||||
this.theme = currentTheme;
|
||||
} catch (error) {
|
||||
console.error("setting user theme error", error);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { action, observable, runInAction, makeObservable } from "mobx";
|
||||
// plane internal packages
|
||||
import { EUserStatus, type TUserStatus } from "@plane/constants";
|
||||
import { AuthService, UserService } from "@plane/services";
|
||||
import type { IUser } from "@plane/types";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
export interface IUserStore {
|
||||
// observables
|
||||
isLoading: boolean;
|
||||
userStatus: TUserStatus | undefined;
|
||||
isUserLoggedIn: boolean | undefined;
|
||||
currentUser: IUser | undefined;
|
||||
// fetch actions
|
||||
hydrate: (data: any) => void;
|
||||
fetchCurrentUser: () => Promise<IUser>;
|
||||
reset: () => void;
|
||||
signOut: () => void;
|
||||
}
|
||||
|
||||
export class UserStore implements IUserStore {
|
||||
// observables
|
||||
isLoading: boolean = true;
|
||||
userStatus: TUserStatus | undefined = undefined;
|
||||
isUserLoggedIn: boolean | undefined = undefined;
|
||||
currentUser: IUser | undefined = undefined;
|
||||
// services
|
||||
userService;
|
||||
authService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isLoading: observable.ref,
|
||||
userStatus: observable,
|
||||
isUserLoggedIn: observable.ref,
|
||||
currentUser: observable,
|
||||
// action
|
||||
fetchCurrentUser: action,
|
||||
reset: action,
|
||||
signOut: action,
|
||||
});
|
||||
this.userService = new UserService();
|
||||
this.authService = new AuthService();
|
||||
}
|
||||
|
||||
hydrate = (data: any) => {
|
||||
if (data) this.currentUser = data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Fetches the current user
|
||||
* @returns Promise<IUser>
|
||||
*/
|
||||
fetchCurrentUser = async () => {
|
||||
try {
|
||||
if (this.currentUser === undefined) this.isLoading = true;
|
||||
const currentUser = await this.userService.adminDetails();
|
||||
if (currentUser) {
|
||||
await this.store.instance.fetchInstanceAdmins();
|
||||
runInAction(() => {
|
||||
this.isUserLoggedIn = true;
|
||||
this.currentUser = currentUser;
|
||||
this.isLoading = false;
|
||||
});
|
||||
} else {
|
||||
runInAction(() => {
|
||||
this.isUserLoggedIn = false;
|
||||
this.currentUser = undefined;
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
return currentUser;
|
||||
} catch (error: any) {
|
||||
this.isLoading = false;
|
||||
this.isUserLoggedIn = false;
|
||||
if (error.status === 403)
|
||||
this.userStatus = {
|
||||
status: EUserStatus.AUTHENTICATION_NOT_DONE,
|
||||
message: error?.message || "",
|
||||
};
|
||||
else
|
||||
this.userStatus = {
|
||||
status: EUserStatus.ERROR,
|
||||
message: error?.message || "",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
reset = async () => {
|
||||
this.isUserLoggedIn = false;
|
||||
this.currentUser = undefined;
|
||||
this.isLoading = false;
|
||||
this.userStatus = undefined;
|
||||
};
|
||||
|
||||
signOut = async () => {
|
||||
this.store.resetOnSignOut();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import set from "lodash/set";
|
||||
import { action, observable, runInAction, makeObservable, computed } from "mobx";
|
||||
// plane imports
|
||||
import { InstanceWorkspaceService } from "@plane/services";
|
||||
import type { IWorkspace, TLoader, TPaginationInfo } from "@plane/types";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
export interface IWorkspaceStore {
|
||||
// observables
|
||||
loader: TLoader;
|
||||
workspaces: Record<string, IWorkspace>;
|
||||
paginationInfo: TPaginationInfo | undefined;
|
||||
// computed
|
||||
workspaceIds: string[];
|
||||
// helper actions
|
||||
hydrate: (data: Record<string, IWorkspace>) => void;
|
||||
getWorkspaceById: (workspaceId: string) => IWorkspace | undefined;
|
||||
// fetch actions
|
||||
fetchWorkspaces: () => Promise<IWorkspace[]>;
|
||||
fetchNextWorkspaces: () => Promise<IWorkspace[]>;
|
||||
// curd actions
|
||||
createWorkspace: (data: IWorkspace) => Promise<IWorkspace>;
|
||||
}
|
||||
|
||||
export class WorkspaceStore implements IWorkspaceStore {
|
||||
// observables
|
||||
loader: TLoader = "init-loader";
|
||||
workspaces: Record<string, IWorkspace> = {};
|
||||
paginationInfo: TPaginationInfo | undefined = undefined;
|
||||
// services
|
||||
instanceWorkspaceService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
loader: observable,
|
||||
workspaces: observable,
|
||||
paginationInfo: observable,
|
||||
// computed
|
||||
workspaceIds: computed,
|
||||
// helper actions
|
||||
hydrate: action,
|
||||
getWorkspaceById: action,
|
||||
// fetch actions
|
||||
fetchWorkspaces: action,
|
||||
fetchNextWorkspaces: action,
|
||||
// curd actions
|
||||
createWorkspace: action,
|
||||
});
|
||||
this.instanceWorkspaceService = new InstanceWorkspaceService();
|
||||
}
|
||||
|
||||
// computed
|
||||
get workspaceIds() {
|
||||
return Object.keys(this.workspaces);
|
||||
}
|
||||
|
||||
// helper actions
|
||||
/**
|
||||
* @description Hydrates the workspaces
|
||||
* @param data - Record<string, IWorkspace>
|
||||
*/
|
||||
hydrate = (data: Record<string, IWorkspace>) => {
|
||||
if (data) this.workspaces = data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Gets a workspace by id
|
||||
* @param workspaceId - string
|
||||
* @returns IWorkspace | undefined
|
||||
*/
|
||||
getWorkspaceById = (workspaceId: string) => this.workspaces[workspaceId];
|
||||
|
||||
// fetch actions
|
||||
/**
|
||||
* @description Fetches all workspaces
|
||||
* @returns Promise<>
|
||||
*/
|
||||
fetchWorkspaces = async (): Promise<IWorkspace[]> => {
|
||||
try {
|
||||
if (this.workspaceIds.length > 0) {
|
||||
this.loader = "mutation";
|
||||
} else {
|
||||
this.loader = "init-loader";
|
||||
}
|
||||
const paginatedWorkspaceData = await this.instanceWorkspaceService.list();
|
||||
runInAction(() => {
|
||||
const { results, ...paginationInfo } = paginatedWorkspaceData;
|
||||
results.forEach((workspace: IWorkspace) => {
|
||||
set(this.workspaces, [workspace.id], workspace);
|
||||
});
|
||||
set(this, "paginationInfo", paginationInfo);
|
||||
});
|
||||
return paginatedWorkspaceData.results;
|
||||
} catch (error) {
|
||||
console.error("Error fetching workspaces", error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.loader = "loaded";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Fetches the next page of workspaces
|
||||
* @returns Promise<IWorkspace[]>
|
||||
*/
|
||||
fetchNextWorkspaces = async (): Promise<IWorkspace[]> => {
|
||||
if (!this.paginationInfo || this.paginationInfo.next_page_results === false) return [];
|
||||
try {
|
||||
this.loader = "pagination";
|
||||
const paginatedWorkspaceData = await this.instanceWorkspaceService.list(this.paginationInfo.next_cursor);
|
||||
runInAction(() => {
|
||||
const { results, ...paginationInfo } = paginatedWorkspaceData;
|
||||
results.forEach((workspace: IWorkspace) => {
|
||||
set(this.workspaces, [workspace.id], workspace);
|
||||
});
|
||||
set(this, "paginationInfo", paginationInfo);
|
||||
});
|
||||
return paginatedWorkspaceData.results;
|
||||
} catch (error) {
|
||||
console.error("Error fetching next workspaces", error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.loader = "loaded";
|
||||
}
|
||||
};
|
||||
|
||||
// curd actions
|
||||
/**
|
||||
* @description Creates a new workspace
|
||||
* @param data - IWorkspace
|
||||
* @returns Promise<IWorkspace>
|
||||
*/
|
||||
createWorkspace = async (data: IWorkspace): Promise<IWorkspace> => {
|
||||
try {
|
||||
this.loader = "mutation";
|
||||
const workspace = await this.instanceWorkspaceService.create(data);
|
||||
runInAction(() => {
|
||||
set(this.workspaces, [workspace.id], workspace);
|
||||
});
|
||||
return workspace;
|
||||
} catch (error) {
|
||||
console.error("Error creating workspace", error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.loader = "loaded";
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "ce/components/authentication/authentication-modes";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./authentication-modes";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "ce/components/common";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "ce/store/root.store";
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "godmode",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "react-router build",
|
||||
"dev": "react-router dev",
|
||||
"start": "react-router-serve ./build/server/index.js",
|
||||
"typecheck": "react-router typegen && tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@plane/constants": "workspace:*",
|
||||
"@plane/hooks": "workspace:*",
|
||||
"@plane/i18n": "workspace:*",
|
||||
"@plane/propel": "workspace:*",
|
||||
"@plane/services": "workspace:*",
|
||||
"@plane/types": "workspace:*",
|
||||
"@plane/ui": "workspace:*",
|
||||
"@plane/utils": "workspace:*",
|
||||
"@react-router/fs-routes": "^7.9.1",
|
||||
"@react-router/node": "^7.7.1",
|
||||
"@react-router/serve": "^7.7.1",
|
||||
"isbot": "^5.1.27",
|
||||
"lodash": "catalog:",
|
||||
"lucide-react": "catalog:",
|
||||
"mobx": "catalog:",
|
||||
"mobx-react": "catalog:",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "7.51.5",
|
||||
"react-router": "^7.7.1",
|
||||
"swr": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@react-router/dev": "^7.7.1",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@types/lodash": "catalog:",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.3",
|
||||
"vite-plugin-commonjs": "^0.10.4",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<svg width="1512" height="900" viewBox="0 0 1512 900" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4817_18724)">
|
||||
<rect width="1512" height="900" fill="#1B1C1E"/>
|
||||
<g opacity="0.09">
|
||||
<line x1="-10.6172" y1="624.328" x2="1500.96" y2="624.328" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="301.59" x2="1500.96" y2="301.59" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="462.958" x2="1500.96" y2="462.958" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="785.696" x2="1500.96" y2="785.696" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="140.22" x2="1500.96" y2="140.22" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="543.642" x2="1500.96" y2="543.642" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="866.381" x2="1500.96" y2="866.381" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="220.904" x2="1500.96" y2="220.904" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="382.272" x2="1500.96" y2="382.272" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="705.012" x2="1500.96" y2="705.013" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="59.534" x2="1500.96" y2="59.534" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="36.3273" y1="-49.8457" x2="36.3273" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="681.808" y1="-49.8457" x2="681.808" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="359.068" y1="-49.8457" x2="359.068" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="1004.54" y1="-49.8457" x2="1004.54" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="1327.28" y1="-49.8457" x2="1327.28" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="197.698" y1="-49.8457" x2="197.698" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="843.173" y1="-49.8457" x2="843.173" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="520.439" y1="-49.8457" x2="520.439" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="1165.92" y1="-49.8457" x2="1165.92" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="1488.66" y1="-49.8457" x2="1488.66" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="117.015" y1="-49.8457" x2="117.015" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="762.491" y1="-49.8457" x2="762.491" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="439.751" y1="-49.8457" x2="439.751" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="1085.23" y1="-49.8457" x2="1085.23" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="1407.97" y1="-49.8457" x2="1407.97" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="278.384" y1="-49.8457" x2="278.384" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="923.861" y1="-49.8457" x2="923.86" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="601.12" y1="-49.8457" x2="601.12" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
<line x1="1246.6" y1="-49.8457" x2="1246.6" y2="1117.56" stroke="white" stroke-opacity="0.18" stroke-width="0.6"/>
|
||||
</g>
|
||||
<g opacity="0.5">
|
||||
<rect x="440.141" y="785.309" width="80.2262" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1165.39" y="221.433" width="80.8965" height="80" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="520.367" y="463.207" width="80.2262" height="79.0801" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1085.39" y="301.659" width="80.2262" height="80.3408" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1166" y="382" width="80.2262" height="80.08" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1247" y="301" width="80.2262" height="81.08" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="439.994" y="221.433" width="80.2262" height="79.0801" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1085.39" y="221.433" width="80" height="80" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="439.994" y="463.207" width="80.2262" height="79.0801" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="359.914" y="785.309" width="80.2262" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="359.914" y="865.535" width="80.2262" height="81.3723" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="198.314" y="705.082" width="80.2262" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1005.16" y="59.835" width="80.2262" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="924.059" y="-20.8525" width="80.2262" height="79.5062" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="924.059" y="59.4053" width="80.2262" height="80.0285" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="116.943" y="58.6885" width="81.3723" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="36.7168" y="59.835" width="81.3723" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="116.943" y="138.915" width="81.3723" height="82.5184" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="440.141" y="-21.5381" width="81.3723" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="198.316" y="300.513" width="80.2262" height="81.3723" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1166.76" y="705.082" width="80.2262" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1246.99" y="785.309" width="80.2262" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="37" y="220" width="81" height="82" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="-44" y="140" width="81" height="82" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4817_18724">
|
||||
<rect width="1512" height="900" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1,68 @@
|
||||
<svg width="1512" height="900" viewBox="0 0 1512 900" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4817_18582)">
|
||||
<rect width="1512" height="900" fill="white"/>
|
||||
<g opacity="0.09">
|
||||
<line x1="-10.6172" y1="625.328" x2="1500.96" y2="625.328" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="302.59" x2="1500.96" y2="302.59" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="463.958" x2="1500.96" y2="463.958" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="786.696" x2="1500.96" y2="786.696" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="141.22" x2="1500.96" y2="141.22" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="544.642" x2="1500.96" y2="544.642" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="867.381" x2="1500.96" y2="867.381" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="221.904" x2="1500.96" y2="221.904" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="383.272" x2="1500.96" y2="383.272" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="706.012" x2="1500.96" y2="706.013" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="-10.6172" y1="60.534" x2="1500.96" y2="60.534" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="36.3273" y1="-48.8457" x2="36.3273" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="681.808" y1="-48.8457" x2="681.808" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="359.068" y1="-48.8457" x2="359.068" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="1004.54" y1="-48.8457" x2="1004.54" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="1327.28" y1="-48.8457" x2="1327.28" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="197.698" y1="-48.8457" x2="197.698" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="843.173" y1="-48.8457" x2="843.173" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="520.439" y1="-48.8457" x2="520.439" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="1165.92" y1="-48.8457" x2="1165.92" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="1488.66" y1="-48.8457" x2="1488.66" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="117.015" y1="-48.8457" x2="117.015" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="762.491" y1="-48.8457" x2="762.491" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="439.751" y1="-48.8457" x2="439.751" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="1085.23" y1="-48.8457" x2="1085.23" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="1407.97" y1="-48.8457" x2="1407.97" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="278.384" y1="-48.8457" x2="278.384" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="923.861" y1="-48.8457" x2="923.86" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="601.12" y1="-48.8457" x2="601.12" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<line x1="1246.6" y1="-48.8457" x2="1246.6" y2="1118.56" stroke="#1F2D5C" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
</g>
|
||||
<g opacity="0.5">
|
||||
<rect x="440.141" y="786.309" width="80.2262" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1166.76" y="222.433" width="80.2262" height="79.0801" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="520.367" y="464.207" width="80.2262" height="79.0801" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1085.39" y="302.659" width="80.2262" height="79.0801" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1166" y="383" width="80.2262" height="80.08" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1247" y="302" width="80.2262" height="81.08" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="439.994" y="222.433" width="80.2262" height="79.0801" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1085.39" y="222.433" width="80.2262" height="79.0801" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="439.994" y="464.207" width="80.2262" height="79.0801" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="359.914" y="786.309" width="80.2262" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="359.914" y="866.535" width="80.2262" height="81.3723" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="198.314" y="706.082" width="80.2262" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1005.16" y="60.835" width="80.2262" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="924.059" y="-19.8525" width="80.2262" height="79.5062" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="924.059" y="60.4053" width="80.2262" height="80.0285" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="116.943" y="59.6885" width="81.3723" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="36.7168" y="60.835" width="81.3723" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="116.943" y="139.915" width="81.3723" height="82.5184" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="440.141" y="-20.5381" width="81.3723" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="198.316" y="301.513" width="80.2262" height="81.3723" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1166.76" y="706.082" width="80.2262" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="1246.99" y="786.309" width="80.2262" height="80.2262" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="37" y="221" width="81" height="82" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
<rect x="-44" y="141" width="81" height="82" fill="#3F76FF" fill-opacity="0.07"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4817_18582">
|
||||
<rect width="1512" height="900" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 466 B |
|
After Width: | Height: | Size: 761 B |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{ "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
After Width: | Height: | Size: 954 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,40 @@
|
||||
<svg width="210" height="206" viewBox="0 0 210 206" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="107.5" cy="103" r="102.5" fill="#24252C"/>
|
||||
<path d="M140.625 162.125V148.875C138.868 148.875 137.183 148.177 135.94 146.935C134.698 145.692 134 144.007 134 142.25V135.625C134 132.111 135.396 128.741 137.881 126.256C140.366 123.771 143.736 122.375 147.25 122.375H160.5C164.014 122.375 167.384 123.771 169.869 126.256C172.354 128.741 173.75 132.111 173.75 135.625V142.25C173.75 144.007 173.052 145.692 171.81 146.935C170.567 148.177 168.882 148.875 167.125 148.875" stroke="#454961" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M153.875 122.375V66.0625C153.875 59.9128 151.432 54.015 147.084 49.6665C142.735 45.318 136.837 42.875 130.687 42.875C124.538 42.875 118.64 45.318 114.291 49.6665C109.943 54.015 107.5 59.9128 107.5 66.0625M107.5 138.937C107.5 145.087 105.057 150.985 100.709 155.334C96.36 159.682 90.4622 162.125 84.3125 162.125C78.1628 162.125 72.265 159.682 67.9165 155.334C63.568 150.985 61.125 145.087 61.125 138.937V82.625" stroke="#454961" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M167.125 162.125V148.875H140.625" stroke="#454961" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M47.875 56.125H74.375V42.875" stroke="#454961" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M74.375 56.125C76.1321 56.125 77.8172 56.823 79.0596 58.0654C80.302 59.3078 81 60.9929 81 62.75V69.375C81 72.8891 79.604 76.2593 77.1192 78.7442C74.6343 81.229 71.2641 82.625 67.75 82.625H54.5C50.9859 82.625 47.6157 81.229 45.1308 78.7442C42.646 76.2593 41.25 72.8891 41.25 69.375V62.75C41.25 60.9929 41.948 59.3078 43.1904 58.0654C44.4328 56.823 46.1179 56.125 47.875 56.125V42.875" stroke="#454961" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g filter="url(#filter0_ddd_11437_265561)">
|
||||
<circle cx="107.911" cy="102.911" r="23.7938" fill="#3A5BC7"/>
|
||||
<path d="M114.051 96.7712L101.771 109.052" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M101.771 96.7712L114.051 109.052" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_ddd_11437_265561" x="76.1172" y="74.1177" width="63.5879" height="64.5876" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feMorphology radius="2" operator="erode" in="SourceAlpha" result="effect1_dropShadow_11437_265561"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0 0 0 0.051 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_11437_265561"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feMorphology radius="4" operator="erode" in="SourceAlpha" result="effect2_dropShadow_11437_265561"/>
|
||||
<feOffset dy="3"/>
|
||||
<feGaussianBlur stdDeviation="6"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.055 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_11437_265561" result="effect2_dropShadow_11437_265561"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feMorphology radius="8" operator="erode" in="SourceAlpha" result="effect3_dropShadow_11437_265561"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.078 0"/>
|
||||
<feBlend mode="normal" in2="effect2_dropShadow_11437_265561" result="effect3_dropShadow_11437_265561"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_11437_265561" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,40 @@
|
||||
<svg width="210" height="206" viewBox="0 0 210 206" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="107.5" cy="103" r="102.5" fill="#F3F6FF"/>
|
||||
<path d="M140.625 162.125V148.875C138.868 148.875 137.183 148.177 135.94 146.935C134.698 145.692 134 144.007 134 142.25V135.625C134 132.111 135.396 128.741 137.881 126.256C140.366 123.771 143.736 122.375 147.25 122.375H160.5C164.014 122.375 167.384 123.771 169.869 126.256C172.354 128.741 173.75 132.111 173.75 135.625V142.25C173.75 144.007 173.052 145.692 171.81 146.935C170.567 148.177 168.882 148.875 167.125 148.875" stroke="#3E63DD" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M153.875 122.375V66.0625C153.875 59.9128 151.432 54.015 147.084 49.6665C142.735 45.318 136.837 42.875 130.687 42.875C124.538 42.875 118.64 45.318 114.291 49.6665C109.943 54.015 107.5 59.9128 107.5 66.0625M107.5 138.937C107.5 145.087 105.057 150.985 100.709 155.334C96.36 159.682 90.4622 162.125 84.3125 162.125C78.1628 162.125 72.265 159.682 67.9165 155.334C63.568 150.985 61.125 145.087 61.125 138.937V82.625" stroke="#3E63DD" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M167.125 162.125V148.875H140.625" stroke="#3E63DD" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M47.875 56.125H74.375V42.875" stroke="#3E63DD" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M74.375 56.125C76.1321 56.125 77.8172 56.823 79.0596 58.0654C80.302 59.3078 81 60.9929 81 62.75V69.375C81 72.8891 79.604 76.2593 77.1192 78.7442C74.6343 81.229 71.2641 82.625 67.75 82.625H54.5C50.9859 82.625 47.6157 81.229 45.1308 78.7442C42.646 76.2593 41.25 72.8891 41.25 69.375V62.75C41.25 60.9929 41.948 59.3078 43.1904 58.0654C44.4328 56.823 46.1179 56.125 47.875 56.125V42.875" stroke="#3E63DD" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g filter="url(#filter0_ddd_11424_265422)">
|
||||
<circle cx="107.911" cy="102.911" r="23.7938" fill="#3A5BC7"/>
|
||||
<path d="M114.051 96.7712L101.771 109.052" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M101.771 96.7712L114.051 109.052" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_ddd_11424_265422" x="76.1172" y="74.1177" width="63.5879" height="64.5876" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feMorphology radius="2" operator="erode" in="SourceAlpha" result="effect1_dropShadow_11424_265422"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0 0 0 0.051 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_11424_265422"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feMorphology radius="4" operator="erode" in="SourceAlpha" result="effect2_dropShadow_11424_265422"/>
|
||||
<feOffset dy="3"/>
|
||||
<feGaussianBlur stdDeviation="6"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.055 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_11424_265422" result="effect2_dropShadow_11424_265422"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feMorphology radius="8" operator="erode" in="SourceAlpha" result="effect3_dropShadow_11424_265422"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.078 0"/>
|
||||
<feBlend mode="normal" in2="effect2_dropShadow_11424_265422" result="effect3_dropShadow_11424_265422"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_11424_265422" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="80 80 220 220"><defs><style>.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style></defs><g id="LOGO"><path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-2" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-3" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/><path class="cls-2" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/><path d="M1 1h22v22H1z" fill="none"/></svg>
|
||||
|
After Width: | Height: | Size: 742 B |
@@ -0,0 +1,11 @@
|
||||
<svg width="92" height="84" viewBox="0 0 92 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3695_11896)">
|
||||
<path d="M83.0398 32.6876C74.2901 27.2397 62.0735 23.8553 48.7013 23.8553C21.7918 23.8553 0 37.3101 0 53.9016C0 69.0898 18.1598 81.554 41.685 83.7001V74.9504C25.8364 72.9693 13.95 64.3022 13.95 53.9016C13.95 42.0977 29.4684 32.44 48.7013 32.44C58.2765 32.44 66.9436 34.8338 73.217 38.7134L64.3022 44.2439H92.1197V27.0746L83.0398 32.6876Z" fill="#CCCCCC"/>
|
||||
<path d="M41.6846 8.99736V74.9504V83.7002L55.6346 74.9504V0L41.6846 8.99736Z" fill="#FF6200"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3695_11896">
|
||||
<rect width="92" height="84" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 702 B |
@@ -0,0 +1,17 @@
|
||||
<svg width="700" height="650" viewBox="0 0 700 650" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3262_5767)">
|
||||
<mask id="mask0_3262_5767" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="700" height="650">
|
||||
<path d="M700 0H0V650H700V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_3262_5767)">
|
||||
<path d="M337.682 0L360.832 20.5C377.982 35.7 395.182 50.85 412.132 66.25C521.982 166 614.982 278.25 684.982 407.45C688.582 414.05 691.832 420.85 695.082 427.6L699.982 437.75L694.582 440.6L690.532 434.85L680.032 419.9L672.682 409.2C621.732 335.25 570.682 261.2 500.582 201.95C479.373 183.995 455.969 168.807 430.932 156.75C380.232 132.5 335.132 142.2 296.432 182C259.632 219.85 240.532 266.85 223.282 314.65C221.032 320.8 218.682 326.9 216.332 333L212.232 343.75L203.632 341C208.632 323.6 213.232 306.1 217.832 288.55C228.332 248.8 238.832 209.05 253.432 170.75C268.932 129.95 288.532 90.6 308.082 51.25C316.532 34.2 324.982 17.15 333.082 0H337.682Z" fill="#C22E33"/>
|
||||
<path d="M372.382 491.1C291.082 529.6 94.3829 569.3 1.08287 559.1C-14.1671 478.8 135.482 102.5 208.982 45.5L204.232 56.4C202.115 61.531 199.813 66.5842 197.332 71.55L194.032 78C156.032 151.1 118.082 224.3 98.6329 304.5C91.6287 332.124 87.8038 360.458 87.2328 388.95C86.7328 455.95 128.432 501.55 198.082 504.4C231.582 505.75 265.432 502.25 299.232 498.7C313.932 497.2 328.582 495.65 343.232 494.5C348.632 494.1 353.932 493.45 360.832 492.55L372.382 491.15V491.1Z" fill="#C22E33"/>
|
||||
<path d="M141.233 639.05C118.983 640.75 96.733 642.45 74.583 644.45C279.433 663.95 476.083 630.6 670.083 562.25C606.833 450.75 521.583 362.7 422.483 286.15C423.783 291.05 426.683 294.6 429.533 298.1L431.933 301.1C440.433 312.4 449.333 323.5 458.283 334.6C478.733 360.05 499.183 385.5 514.583 413.5C553.483 484.5 532.383 545.9 456.183 578.3C406.083 599.65 351.333 614.2 297.183 622.9C245.683 631.1 193.433 635.05 141.233 639.05Z" fill="#C22E33"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3262_5767">
|
||||
<rect width="700" height="650" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,35 @@
|
||||
<svg width="91" height="90" viewBox="0 0 91 90" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="45.1309" cy="45" r="45" fill="#1F2D5C"/>
|
||||
<path d="M61.2694 60.8093H29.1265C28.6487 60.8093 28.2578 60.4184 28.2578 59.9406V59.0719C28.2578 58.5941 28.6487 58.2031 29.1265 58.2031H61.2694C61.7472 58.2031 62.1381 58.5941 62.1381 59.0719V59.9406C62.1381 60.4184 61.7472 60.8093 61.2694 60.8093Z" fill="url(#paint0_linear_2783_1138)"/>
|
||||
<path d="M51.341 35.5623L39.1059 29.8104C38.0009 29.2909 36.6891 29.5203 35.8256 30.3838L35.168 31.0414L43.387 38.1485L51.341 35.5623Z" fill="url(#paint1_linear_2783_1138)"/>
|
||||
<path d="M61.0698 36.2872L59.2611 31.4023L33.7223 41.1182L31.1092 38.8178C30.389 38.1836 29.3561 38.049 28.4969 38.4772L28.324 38.5633C28.1034 38.6736 28.0069 38.9368 28.1042 39.1627L30.7825 45.4062L33.9933 46.7666L61.0698 36.2872Z" fill="url(#paint2_linear_2783_1138)"/>
|
||||
<path d="M60.073 36.4877C61.5123 36.4877 62.6792 35.3209 62.6792 33.8816C62.6792 32.4422 61.5123 31.2754 60.073 31.2754C58.6336 31.2754 57.4668 32.4422 57.4668 33.8816C57.4668 35.3209 58.6336 36.4877 60.073 36.4877Z" fill="url(#paint3_linear_2783_1138)"/>
|
||||
<path d="M33.1433 46.9116C34.5826 46.9116 35.7495 45.7447 35.7495 44.3054C35.7495 42.866 34.5826 41.6992 33.1433 41.6992C31.7039 41.6992 30.5371 42.866 30.5371 44.3054C30.5371 45.7447 31.7039 46.9116 33.1433 46.9116Z" fill="url(#paint4_linear_2783_1138)"/>
|
||||
<path d="M45.3319 40.3661C45.0304 40.4799 44.8228 40.7587 44.7993 41.0802L43.8516 54.1501H44.7811C46.0017 54.1501 47.0919 53.3839 47.5046 52.2363L52.5154 38.3376C52.6292 38.0231 52.3208 37.7199 52.008 37.839L45.3319 40.3661Z" fill="url(#paint5_linear_2783_1138)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2783_1138" x1="35.9269" y1="50.2343" x2="53.6594" y2="67.9676" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#74A0E2"/>
|
||||
<stop offset="1" stop-color="#4F90EF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2783_1138" x1="35.168" y1="33.8413" x2="51.341" y2="33.8413" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3079D6"/>
|
||||
<stop offset="1" stop-color="#297CD2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2783_1138" x1="28.066" y1="39.0854" x2="61.0698" y2="39.0854" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#42A3F2"/>
|
||||
<stop offset="1" stop-color="#42A4EB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_2783_1138" x1="57.4668" y1="33.8816" x2="62.6792" y2="33.8816" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#42A3F2"/>
|
||||
<stop offset="1" stop-color="#42A4EB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_2783_1138" x1="30.5371" y1="44.3054" x2="35.7495" y2="44.3054" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#42A3F2"/>
|
||||
<stop offset="1" stop-color="#42A4EB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_2783_1138" x1="43.8516" y1="45.9815" x2="52.5397" y2="45.9815" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3079D6"/>
|
||||
<stop offset="1" stop-color="#297CD2"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,40 @@
|
||||
<svg width="91" height="90" viewBox="0 0 91 90" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_8747_25960)">
|
||||
<circle cx="45.1309" cy="45" r="45" fill="#F7F9FF"/>
|
||||
<path d="M61.2694 60.8093H29.1265C28.6487 60.8093 28.2578 60.4184 28.2578 59.9406V59.0719C28.2578 58.5941 28.6487 58.2031 29.1265 58.2031H61.2694C61.7472 58.2031 62.1381 58.5941 62.1381 59.0719V59.9406C62.1381 60.4184 61.7472 60.8093 61.2694 60.8093Z" fill="url(#paint0_linear_8747_25960)"/>
|
||||
<path d="M51.341 35.5623L39.1059 29.8104C38.0009 29.2909 36.6891 29.5203 35.8256 30.3838L35.168 31.0414L43.387 38.1485L51.341 35.5623Z" fill="url(#paint1_linear_8747_25960)"/>
|
||||
<path d="M61.0698 36.2872L59.2611 31.4023L33.7223 41.1182L31.1092 38.8178C30.389 38.1836 29.3561 38.049 28.4969 38.4772L28.324 38.5633C28.1034 38.6736 28.0069 38.9368 28.1042 39.1627L30.7825 45.4062L33.9933 46.7666L61.0698 36.2872Z" fill="url(#paint2_linear_8747_25960)"/>
|
||||
<path d="M60.073 36.4858C61.5123 36.4858 62.6792 35.319 62.6792 33.8796C62.6792 32.4403 61.5123 31.2734 60.073 31.2734C58.6336 31.2734 57.4668 32.4403 57.4668 33.8796C57.4668 35.319 58.6336 36.4858 60.073 36.4858Z" fill="url(#paint3_linear_8747_25960)"/>
|
||||
<path d="M33.1433 46.9116C34.5826 46.9116 35.7495 45.7447 35.7495 44.3054C35.7495 42.866 34.5826 41.6992 33.1433 41.6992C31.7039 41.6992 30.5371 42.866 30.5371 44.3054C30.5371 45.7447 31.7039 46.9116 33.1433 46.9116Z" fill="url(#paint4_linear_8747_25960)"/>
|
||||
<path d="M45.3319 40.3661C45.0304 40.4799 44.8228 40.7587 44.7993 41.0802L43.8516 54.1501H44.7811C46.0017 54.1501 47.0919 53.3839 47.5046 52.2363L52.5154 38.3376C52.6292 38.0231 52.3208 37.7199 52.008 37.839L45.3319 40.3661Z" fill="url(#paint5_linear_8747_25960)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_8747_25960" x1="35.9269" y1="50.2343" x2="53.6594" y2="67.9676" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1D59B3"/>
|
||||
<stop offset="1" stop-color="#195BBC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_8747_25960" x1="35.168" y1="33.8413" x2="51.341" y2="33.8413" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3079D6"/>
|
||||
<stop offset="1" stop-color="#297CD2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_8747_25960" x1="28.066" y1="39.0854" x2="61.0698" y2="39.0854" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#42A3F2"/>
|
||||
<stop offset="1" stop-color="#42A4EB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_8747_25960" x1="57.4668" y1="33.8796" x2="62.6792" y2="33.8796" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#42A3F2"/>
|
||||
<stop offset="1" stop-color="#42A4EB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_8747_25960" x1="30.5371" y1="44.3054" x2="35.7495" y2="44.3054" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#42A3F2"/>
|
||||
<stop offset="1" stop-color="#42A4EB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_8747_25960" x1="43.8516" y1="45.9815" x2="52.5397" y2="45.9815" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3079D6"/>
|
||||
<stop offset="1" stop-color="#297CD2"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_8747_25960">
|
||||
<rect width="90" height="90" fill="white" transform="translate(0.130859)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Plane God Mode",
|
||||
"short_name": "Plane God Mode",
|
||||
"description": "Plane helps you plan your issues, cycles, and product modules.",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#f9fafb",
|
||||
"theme_color": "#3f76ff",
|
||||
"icons": [
|
||||
{ "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Config } from "@react-router/dev/config";
|
||||
|
||||
export default {
|
||||
// Config options...
|
||||
// Server-side render by default, to enable SPA mode set this to `false`
|
||||
ssr: false,
|
||||
} satisfies Config;
|
||||
@@ -0,0 +1,374 @@
|
||||
// OPTION 1: FOLDER-BASED STRUCTURE (Like Next.js)
|
||||
// This creates the same routes as Next.js folder structure
|
||||
|
||||
app/routes/
|
||||
├── _index.tsx # → /
|
||||
├── about.tsx # → /about
|
||||
├── blog/
|
||||
│ ├── route.tsx # → /blog (layout)
|
||||
│ ├── _index.tsx # → /blog
|
||||
│ ├── $slug.tsx # → /blog/:slug
|
||||
│ └── categories/
|
||||
│ ├── route.tsx # → /blog/categories (layout)
|
||||
│ ├── _index.tsx # → /blog/categories
|
||||
│ └── $categoryId.tsx # → /blog/categories/:categoryId
|
||||
├── dashboard/
|
||||
│ ├── route.tsx # → /dashboard (layout)
|
||||
│ ├── _index.tsx # → /dashboard
|
||||
│ ├── settings.tsx # → /dashboard/settings
|
||||
│ ├── users/
|
||||
│ │ ├── route.tsx # → /dashboard/users (layout)
|
||||
│ │ ├── _index.tsx # → /dashboard/users
|
||||
│ │ ├── $userId.tsx # → /dashboard/users/:userId
|
||||
│ │ └── $userId.edit.tsx # → /dashboard/users/:userId/edit
|
||||
│ └── analytics.tsx # → /dashboard/analytics
|
||||
└── auth/
|
||||
├── login.tsx # → /auth/login
|
||||
└── register.tsx # → /auth/register
|
||||
|
||||
// OPTION 2: FLAT STRUCTURE (All at root level)
|
||||
app/routes/
|
||||
├── _index.tsx # → /
|
||||
├── about.tsx # → /about
|
||||
├── blog.tsx # → /blog (layout)
|
||||
├── blog._index.tsx # → /blog
|
||||
├── blog.$slug.tsx # → /blog/:slug
|
||||
├── blog.categories.tsx # → /blog/categories (layout)
|
||||
├── blog.categories._index.tsx # → /blog/categories
|
||||
├── blog.categories.$categoryId.tsx # → /blog/categories/:categoryId
|
||||
├── dashboard.tsx # → /dashboard (layout)
|
||||
├── dashboard._index.tsx # → /dashboard
|
||||
├── dashboard.settings.tsx # → /dashboard/settings
|
||||
├── dashboard.users.tsx # → /dashboard/users (layout)
|
||||
├── dashboard.users._index.tsx # → /dashboard/users
|
||||
├── dashboard.users.$userId.tsx # → /dashboard/users/:userId
|
||||
├── dashboard.users.$userId.edit.tsx # → /dashboard/users/:userId/edit
|
||||
├── dashboard.analytics.tsx # → /dashboard/analytics
|
||||
├── auth.login.tsx # → /auth/login
|
||||
└── auth.register.tsx # → /auth/register
|
||||
|
||||
// OPTION 3: HYBRID APPROACH (Mix of both)
|
||||
app/routes/
|
||||
├── _index.tsx # → /
|
||||
├── about.tsx # → /about
|
||||
├── blog/ # Folder for complex sections
|
||||
│ ├── route.tsx
|
||||
│ ├── _index.tsx
|
||||
│ ├── $slug.tsx
|
||||
│ └── categories/
|
||||
│ ├── route.tsx
|
||||
│ ├── _index.tsx
|
||||
│ └── $categoryId.tsx
|
||||
├── dashboard.tsx # Simple layout at root
|
||||
├── dashboard._index.tsx
|
||||
├── dashboard.settings.tsx
|
||||
├── dashboard.analytics.tsx
|
||||
└── auth.login.tsx # Simple routes at root
|
||||
|
||||
// EXAMPLES OF FOLDER-BASED ROUTES
|
||||
|
||||
// app/routes/blog/route.tsx - Blog layout
|
||||
import { Outlet } from "react-router";
|
||||
import type { LoaderFunction } from "react-router";
|
||||
|
||||
export const loader: LoaderFunction = async () => {
|
||||
const categories = await getBlogCategories();
|
||||
return { categories };
|
||||
};
|
||||
|
||||
export default function BlogLayout() {
|
||||
const { categories } = useLoaderData();
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 py-8">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl font-bold">Our Blog</h1>
|
||||
<nav className="mt-4">
|
||||
<ul className="flex space-x-4">
|
||||
<li><Link to="/blog">All Posts</Link></li>
|
||||
{categories.map(cat => (
|
||||
<li key={cat.id}>
|
||||
<Link to={`/blog/categories/${cat.id}`}>{cat.name}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// app/routes/blog/_index.tsx - Blog homepage
|
||||
import type { LoaderFunction } from "react-router";
|
||||
|
||||
export const loader: LoaderFunction = async () => {
|
||||
const posts = await getBlogPosts();
|
||||
return { posts };
|
||||
};
|
||||
|
||||
export default function BlogIndex() {
|
||||
const { posts } = useLoaderData();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-6">Latest Posts</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map(post => (
|
||||
<article key={post.id} className="bg-white rounded-lg shadow p-6">
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
<Link to={`/blog/${post.slug}`} className="hover:text-blue-600">
|
||||
{post.title}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">{post.excerpt}</p>
|
||||
<p className="text-sm text-gray-500">{post.publishedAt}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// app/routes/blog/$slug.tsx - Individual blog post
|
||||
import type { LoaderFunction } from "react-router";
|
||||
|
||||
export const loader: LoaderFunction = async ({ params }) => {
|
||||
const post = await getBlogPost(params.slug);
|
||||
if (!post) {
|
||||
throw new Response("Post not found", { status: 404 });
|
||||
}
|
||||
return { post };
|
||||
};
|
||||
|
||||
export default function BlogPost() {
|
||||
const { post } = useLoaderData();
|
||||
|
||||
return (
|
||||
<article className="max-w-3xl">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
|
||||
<div className="flex items-center text-gray-600 mb-4">
|
||||
<span>By {post.author}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>{post.publishedAt}</span>
|
||||
</div>
|
||||
{post.featuredImage && (
|
||||
<img
|
||||
src={post.featuredImage}
|
||||
alt={post.title}
|
||||
className="w-full h-64 object-cover rounded-lg"
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div
|
||||
className="prose prose-lg max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
/>
|
||||
|
||||
<footer className="mt-8 pt-8 border-t">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{post.tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
// app/routes/dashboard/users/route.tsx - Nested users layout
|
||||
import { Outlet, Link, useLocation } from "react-router";
|
||||
|
||||
export default function UsersLayout() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">User Management</h1>
|
||||
<Link
|
||||
to="/dashboard/users/new"
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Add New User
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Sub-navigation */}
|
||||
<nav className="mb-6">
|
||||
<ul className="flex space-x-4 border-b">
|
||||
<li>
|
||||
<Link
|
||||
to="/dashboard/users"
|
||||
className={`pb-2 px-1 ${
|
||||
location.pathname === '/dashboard/users'
|
||||
? 'border-b-2 border-blue-600 text-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
All Users
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to="/dashboard/users/active"
|
||||
className={`pb-2 px-1 ${
|
||||
location.pathname === '/dashboard/users/active'
|
||||
? 'border-b-2 border-blue-600 text-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Active Users
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// app/routes/dashboard/users/$userId.tsx - User detail page
|
||||
import type { LoaderFunction } from "react-router";
|
||||
|
||||
export const loader: LoaderFunction = async ({ params }) => {
|
||||
const user = await getUser(params.userId);
|
||||
if (!user) {
|
||||
throw new Response("User not found", { status: 404 });
|
||||
}
|
||||
return { user };
|
||||
};
|
||||
|
||||
export default function UserDetail() {
|
||||
const { user } = useLoaderData();
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<img
|
||||
src={user.avatar || '/default-avatar.png'}
|
||||
alt={user.name}
|
||||
className="w-16 h-16 rounded-full"
|
||||
/>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">{user.name}</h2>
|
||||
<p className="text-gray-600">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to={`/dashboard/users/${user.id}/edit`}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Edit User
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Profile Information</h3>
|
||||
<dl className="space-y-2">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Full Name</dt>
|
||||
<dd className="text-sm text-gray-900">{user.name}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Email</dt>
|
||||
<dd className="text-sm text-gray-900">{user.email}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Role</dt>
|
||||
<dd className="text-sm text-gray-900">{user.role}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Status</dt>
|
||||
<dd className="text-sm text-gray-900">
|
||||
<span className={`px-2 py-1 rounded-full text-xs ${
|
||||
user.status === 'active'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{user.status}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Account Details</h3>
|
||||
<dl className="space-y-2">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Member Since</dt>
|
||||
<dd className="text-sm text-gray-900">{user.createdAt}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Last Login</dt>
|
||||
<dd className="text-sm text-gray-900">{user.lastLoginAt}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Total Orders</dt>
|
||||
<dd className="text-sm text-gray-900">{user.orderCount}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// COMPARISON WITH NEXT.JS STRUCTURE
|
||||
|
||||
/*
|
||||
Next.js Structure:
|
||||
pages/
|
||||
├── index.js # /
|
||||
├── about.js # /about
|
||||
├── blog/
|
||||
│ ├── index.js # /blog
|
||||
│ ├── [slug].js # /blog/[slug]
|
||||
│ └── categories/
|
||||
│ ├── index.js # /blog/categories
|
||||
│ └── [categoryId].js # /blog/categories/[categoryId]
|
||||
└── dashboard/
|
||||
├── index.js # /dashboard
|
||||
├── settings.js # /dashboard/settings
|
||||
└── users/
|
||||
├── index.js # /dashboard/users
|
||||
├── [userId].js # /dashboard/users/[userId]
|
||||
└── [userId]/
|
||||
└── edit.js # /dashboard/users/[userId]/edit
|
||||
|
||||
React Router 7 Equivalent (Folder-based):
|
||||
app/routes/
|
||||
├── _index.tsx # /
|
||||
├── about.tsx # /about
|
||||
├── blog/
|
||||
│ ├── route.tsx # /blog (with layout)
|
||||
│ ├── _index.tsx # /blog
|
||||
│ ├── $slug.tsx # /blog/:slug
|
||||
│ └── categories/
|
||||
│ ├── route.tsx # /blog/categories (with layout)
|
||||
│ ├── _index.tsx # /blog/categories
|
||||
│ └── $categoryId.tsx # /blog/categories/:categoryId
|
||||
└── dashboard/
|
||||
├── route.tsx # /dashboard (with layout)
|
||||
├── _index.tsx # /dashboard
|
||||
├── settings.tsx # /dashboard/settings
|
||||
└── users/
|
||||
├── route.tsx # /dashboard/users (with layout)
|
||||
├── _index.tsx # /dashboard/users
|
||||
├── $userId.tsx # /dashboard/users/:userId
|
||||
└── $userId.edit.tsx # /dashboard/users/:userId/edit
|
||||
*/
|
||||
@@ -0,0 +1,452 @@
|
||||
@import "@plane/propel/styles/fonts";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.text-1\.5xl {
|
||||
font-size: 1.375rem;
|
||||
line-height: 1.875rem;
|
||||
}
|
||||
|
||||
.text-2\.5xl {
|
||||
font-size: 1.75rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light !important;
|
||||
|
||||
--color-primary-10: 229, 243, 250;
|
||||
--color-primary-20: 216, 237, 248;
|
||||
--color-primary-30: 199, 229, 244;
|
||||
--color-primary-40: 169, 214, 239;
|
||||
--color-primary-50: 144, 202, 234;
|
||||
--color-primary-60: 109, 186, 227;
|
||||
--color-primary-70: 75, 170, 221;
|
||||
--color-primary-80: 41, 154, 214;
|
||||
--color-primary-90: 34, 129, 180;
|
||||
--color-primary-100: 0, 99, 153;
|
||||
--color-primary-200: 0, 92, 143;
|
||||
--color-primary-300: 0, 86, 133;
|
||||
--color-primary-400: 0, 77, 117;
|
||||
--color-primary-500: 0, 66, 102;
|
||||
--color-primary-600: 0, 53, 82;
|
||||
--color-primary-700: 0, 43, 66;
|
||||
--color-primary-800: 0, 33, 51;
|
||||
--color-primary-900: 0, 23, 36;
|
||||
|
||||
--color-background-100: 255, 255, 255; /* primary bg */
|
||||
--color-background-90: 247, 247, 247; /* secondary bg */
|
||||
--color-background-80: 232, 232, 232; /* tertiary bg */
|
||||
|
||||
--color-text-100: 23, 23, 23; /* primary text */
|
||||
--color-text-200: 58, 58, 58; /* secondary text */
|
||||
--color-text-300: 82, 82, 82; /* tertiary text */
|
||||
--color-text-400: 163, 163, 163; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 163, 163, 163; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 245, 245, 245; /* subtle border= 1 */
|
||||
--color-border-200: 229, 229, 229; /* subtle border- 2 */
|
||||
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
||||
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
||||
|
||||
--color-shadow-2xs:
|
||||
0px 0px 1px 0px rgba(23, 23, 23, 0.06),
|
||||
0px 1px 2px 0px rgba(23, 23, 23, 0.06),
|
||||
0px 1px 2px 0px rgba(23, 23, 23, 0.14);
|
||||
--color-shadow-xs:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.16),
|
||||
0px 2px 4px 0px rgba(16, 24, 40, 0.12),
|
||||
0px 1px 8px -1px rgba(16, 24, 40, 0.1);
|
||||
--color-shadow-sm:
|
||||
0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02),
|
||||
0px 1px 12px 0px rgba(0, 0, 0, 0.12);
|
||||
--color-shadow-rg:
|
||||
0px 3px 6px 0px rgba(0, 0, 0, 0.1),
|
||||
0px 4px 4px 0px rgba(16, 24, 40, 0.08),
|
||||
0px 1px 12px 0px rgba(16, 24, 40, 0.04);
|
||||
--color-shadow-md:
|
||||
0px 4px 8px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 6px 12px 0px rgba(16, 24, 40, 0.12),
|
||||
0px 1px 16px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-lg:
|
||||
0px 6px 12px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 8px 16px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 1px 24px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-xl:
|
||||
0px 0px 18px 0px rgba(0, 0, 0, 0.16),
|
||||
0px 0px 24px 0px rgba(16, 24, 40, 0.16),
|
||||
0px 0px 52px 0px rgba(16, 24, 40, 0.16);
|
||||
--color-shadow-2xl:
|
||||
0px 8px 16px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 12px 24px 0px rgba(16, 24, 40, 0.12),
|
||||
0px 1px 32px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-3xl:
|
||||
0px 12px 24px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 16px 32px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 1px 48px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-4xl:
|
||||
0px 8px 40px 0px rgba(0, 0, 61, 0.05),
|
||||
0px 12px 32px -16px rgba(0, 0, 0, 0.05);
|
||||
|
||||
--color-sidebar-background-100: var(
|
||||
--color-background-100
|
||||
); /* primary sidebar bg */
|
||||
--color-sidebar-background-90: var(
|
||||
--color-background-90
|
||||
); /* secondary sidebar bg */
|
||||
--color-sidebar-background-80: var(
|
||||
--color-background-80
|
||||
); /* tertiary sidebar bg */
|
||||
|
||||
--color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
|
||||
--color-sidebar-text-200: var(
|
||||
--color-text-200
|
||||
); /* secondary sidebar text */
|
||||
--color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
|
||||
--color-sidebar-text-400: var(
|
||||
--color-text-400
|
||||
); /* sidebar placeholder text */
|
||||
|
||||
--color-sidebar-border-100: var(
|
||||
--color-border-100
|
||||
); /* subtle sidebar border= 1 */
|
||||
--color-sidebar-border-200: var(
|
||||
--color-border-100
|
||||
); /* subtle sidebar border- 2 */
|
||||
--color-sidebar-border-300: var(
|
||||
--color-border-100
|
||||
); /* strong sidebar border- 1 */
|
||||
--color-sidebar-border-400: var(
|
||||
--color-border-100
|
||||
); /* strong sidebar border- 2 */
|
||||
|
||||
--color-sidebar-shadow-2xs: var(--color-shadow-2xs);
|
||||
--color-sidebar-shadow-xs: var(--color-shadow-xs);
|
||||
--color-sidebar-shadow-sm: var(--color-shadow-sm);
|
||||
--color-sidebar-shadow-rg: var(--color-shadow-rg);
|
||||
--color-sidebar-shadow-md: var(--color-shadow-md);
|
||||
--color-sidebar-shadow-lg: var(--color-shadow-lg);
|
||||
--color-sidebar-shadow-xl: var(--color-shadow-xl);
|
||||
--color-sidebar-shadow-2xl: var(--color-shadow-2xl);
|
||||
--color-sidebar-shadow-3xl: var(--color-shadow-3xl);
|
||||
--color-sidebar-shadow-4xl: var(--color-shadow-4xl);
|
||||
}
|
||||
|
||||
[data-theme="light"],
|
||||
[data-theme="light-contrast"] {
|
||||
color-scheme: light !important;
|
||||
|
||||
--color-background-100: 255, 255, 255; /* primary bg */
|
||||
--color-background-90: 247, 247, 247; /* secondary bg */
|
||||
--color-background-80: 232, 232, 232; /* tertiary bg */
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--color-text-100: 23, 23, 23; /* primary text */
|
||||
--color-text-200: 58, 58, 58; /* secondary text */
|
||||
--color-text-300: 82, 82, 82; /* tertiary text */
|
||||
--color-text-400: 163, 163, 163; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 163, 163, 163; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 245, 245, 245; /* subtle border= 1 */
|
||||
--color-border-200: 229, 229, 229; /* subtle border- 2 */
|
||||
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
||||
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
||||
|
||||
/* toast theme */
|
||||
--color-toast-success-text: 62, 155, 79;
|
||||
--color-toast-error-text: 220, 62, 66;
|
||||
--color-toast-warning-text: 255, 186, 24;
|
||||
--color-toast-info-text: 51, 88, 212;
|
||||
--color-toast-loading-text: 28, 32, 36;
|
||||
--color-toast-secondary-text: 128, 131, 141;
|
||||
--color-toast-tertiary-text: 96, 100, 108;
|
||||
|
||||
--color-toast-success-background: 253, 253, 254;
|
||||
--color-toast-error-background: 255, 252, 252;
|
||||
--color-toast-warning-background: 254, 253, 251;
|
||||
--color-toast-info-background: 253, 253, 254;
|
||||
--color-toast-loading-background: 253, 253, 254;
|
||||
|
||||
--color-toast-success-border: 218, 241, 219;
|
||||
--color-toast-error-border: 255, 219, 220;
|
||||
--color-toast-warning-border: 255, 247, 194;
|
||||
--color-toast-info-border: 210, 222, 255;
|
||||
--color-toast-loading-border: 224, 225, 230;
|
||||
}
|
||||
|
||||
[data-theme="light-contrast"] {
|
||||
--color-text-100: 11, 11, 11; /* primary text */
|
||||
--color-text-200: 38, 38, 38; /* secondary text */
|
||||
--color-text-300: 58, 58, 58; /* tertiary text */
|
||||
--color-text-400: 115, 115, 115; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 115, 115, 115; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 34, 34, 34; /* subtle border= 1 */
|
||||
--color-border-200: 38, 38, 38; /* subtle border- 2 */
|
||||
--color-border-300: 46, 46, 46; /* strong border- 1 */
|
||||
--color-border-400: 58, 58, 58; /* strong border- 2 */
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
[data-theme="dark-contrast"] {
|
||||
color-scheme: dark !important;
|
||||
|
||||
--color-primary-10: 8, 31, 43;
|
||||
--color-primary-20: 10, 37, 51;
|
||||
--color-primary-30: 13, 49, 69;
|
||||
--color-primary-40: 16, 58, 81;
|
||||
--color-primary-50: 18, 68, 94;
|
||||
--color-primary-60: 23, 86, 120;
|
||||
--color-primary-70: 28, 104, 146;
|
||||
--color-primary-80: 31, 116, 163;
|
||||
--color-primary-90: 34, 129, 180;
|
||||
--color-primary-100: 40, 146, 204;
|
||||
--color-primary-200: 41, 154, 214;
|
||||
--color-primary-300: 75, 170, 221;
|
||||
--color-primary-400: 109, 186, 227;
|
||||
--color-primary-500: 144, 202, 234;
|
||||
--color-primary-600: 169, 214, 239;
|
||||
--color-primary-700: 199, 229, 244;
|
||||
--color-primary-800: 216, 237, 248;
|
||||
--color-primary-900: 229, 243, 250;
|
||||
|
||||
--color-background-100: 25, 25, 25; /* primary bg */
|
||||
--color-background-90: 32, 32, 32; /* secondary bg */
|
||||
--color-background-80: 44, 44, 44; /* tertiary bg */
|
||||
|
||||
--color-shadow-2xs:
|
||||
0px 0px 1px 0px rgba(0, 0, 0, 0.15), 0px 1px 3px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-xs:
|
||||
0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-sm:
|
||||
0px 0px 4px 0px rgba(0, 0, 0, 0.2), 0px 2px 6px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-rg:
|
||||
0px 0px 6px 0px rgba(0, 0, 0, 0.2), 0px 4px 6px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-md:
|
||||
0px 2px 8px 0px rgba(0, 0, 0, 0.2), 0px 4px 8px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-lg:
|
||||
0px 4px 12px 0px rgba(0, 0, 0, 0.25), 0px 4px 10px 0px rgba(0, 0, 0, 0.55);
|
||||
--color-shadow-xl:
|
||||
0px 0px 14px 0px rgba(0, 0, 0, 0.25), 0px 6px 10px 0px rgba(0, 0, 0, 0.55);
|
||||
--color-shadow-2xl:
|
||||
0px 0px 18px 0px rgba(0, 0, 0, 0.25), 0px 8px 12px 0px rgba(0, 0, 0, 0.6);
|
||||
--color-shadow-3xl:
|
||||
0px 4px 24px 0px rgba(0, 0, 0, 0.3), 0px 12px 40px 0px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--color-text-100: 229, 229, 229; /* primary text */
|
||||
--color-text-200: 163, 163, 163; /* secondary text */
|
||||
--color-text-300: 115, 115, 115; /* tertiary text */
|
||||
--color-text-400: 82, 82, 82; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 82, 82, 82; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 34, 34, 34; /* subtle border= 1 */
|
||||
--color-border-200: 38, 38, 38; /* subtle border- 2 */
|
||||
--color-border-300: 46, 46, 46; /* strong border- 1 */
|
||||
--color-border-400: 58, 58, 58; /* strong border- 2 */
|
||||
|
||||
/* toast theme */
|
||||
--color-toast-success-text: 178, 221, 181;
|
||||
--color-toast-error-text: 206, 44, 49;
|
||||
--color-toast-warning-text: 255, 186, 24;
|
||||
--color-toast-info-text: 141, 164, 239;
|
||||
--color-toast-loading-text: 255, 255, 255;
|
||||
--color-toast-secondary-text: 185, 187, 198;
|
||||
--color-toast-tertiary-text: 139, 141, 152;
|
||||
|
||||
--color-toast-success-background: 46, 46, 46;
|
||||
--color-toast-error-background: 46, 46, 46;
|
||||
--color-toast-warning-background: 46, 46, 46;
|
||||
--color-toast-info-background: 46, 46, 46;
|
||||
--color-toast-loading-background: 46, 46, 46;
|
||||
|
||||
--color-toast-success-border: 42, 126, 59;
|
||||
--color-toast-error-border: 100, 23, 35;
|
||||
--color-toast-warning-border: 79, 52, 34;
|
||||
--color-toast-info-border: 58, 91, 199;
|
||||
--color-toast-loading-border: 96, 100, 108;
|
||||
}
|
||||
|
||||
[data-theme="dark-contrast"] {
|
||||
--color-text-100: 250, 250, 250; /* primary text */
|
||||
--color-text-200: 241, 241, 241; /* secondary text */
|
||||
--color-text-300: 212, 212, 212; /* tertiary text */
|
||||
--color-text-400: 115, 115, 115; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 115, 115, 115; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 245, 245, 245; /* subtle border= 1 */
|
||||
--color-border-200: 229, 229, 229; /* subtle border- 2 */
|
||||
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
||||
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
||||
}
|
||||
|
||||
[data-theme="light"],
|
||||
[data-theme="dark"],
|
||||
[data-theme="light-contrast"],
|
||||
[data-theme="dark-contrast"] {
|
||||
--color-sidebar-background-100: var(
|
||||
--color-background-100
|
||||
); /* primary sidebar bg */
|
||||
--color-sidebar-background-90: var(
|
||||
--color-background-90
|
||||
); /* secondary sidebar bg */
|
||||
--color-sidebar-background-80: var(
|
||||
--color-background-80
|
||||
); /* tertiary sidebar bg */
|
||||
|
||||
--color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
|
||||
--color-sidebar-text-200: var(
|
||||
--color-text-200
|
||||
); /* secondary sidebar text */
|
||||
--color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
|
||||
--color-sidebar-text-400: var(
|
||||
--color-text-400
|
||||
); /* sidebar placeholder text */
|
||||
|
||||
--color-sidebar-border-100: var(
|
||||
--color-border-100
|
||||
); /* subtle sidebar border= 1 */
|
||||
--color-sidebar-border-200: var(
|
||||
--color-border-200
|
||||
); /* subtle sidebar border- 2 */
|
||||
--color-sidebar-border-300: var(
|
||||
--color-border-300
|
||||
); /* strong sidebar border- 1 */
|
||||
--color-sidebar-border-400: var(
|
||||
--color-border-400
|
||||
); /* strong sidebar border- 2 */
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
font-variant-ligatures: none;
|
||||
-webkit-font-variant-ligatures: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgba(var(--color-text-100));
|
||||
}
|
||||
|
||||
/* scrollbar style */
|
||||
@-moz-document url-prefix() {
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.vertical-scrollbar,
|
||||
.horizontal-scrollbar {
|
||||
scrollbar-width: initial;
|
||||
scrollbar-color: rgba(96, 100, 108, 0.1) transparent;
|
||||
}
|
||||
.vertical-scrollbar:hover,
|
||||
.horizontal-scrollbar:hover {
|
||||
scrollbar-color: rgba(96, 100, 108, 0.25) transparent;
|
||||
}
|
||||
.vertical-scrollbar:active,
|
||||
.horizontal-scrollbar:active {
|
||||
scrollbar-color: rgba(96, 100, 108, 0.7) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-scrollbar {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.horizontal-scrollbar {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar,
|
||||
.horizontal-scrollbar::-webkit-scrollbar {
|
||||
display: block;
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-track,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-thumb,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-clip: padding-box;
|
||||
background-color: rgba(96, 100, 108, 0.1);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.vertical-scrollbar:hover::-webkit-scrollbar-thumb,
|
||||
.horizontal-scrollbar:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(96, 100, 108, 0.25);
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-thumb:hover,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(96, 100, 108, 0.5);
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-thumb:active,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-thumb:active {
|
||||
background-color: rgba(96, 100, 108, 0.7);
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-corner,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
.vertical-scrollbar-margin-top-md::-webkit-scrollbar-track {
|
||||
margin-top: 44px;
|
||||
}
|
||||
|
||||
/* scrollbar sm size */
|
||||
.scrollbar-sm::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
.scrollbar-sm::-webkit-scrollbar-thumb {
|
||||
border: 3px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
/* scrollbar md size */
|
||||
.scrollbar-md::-webkit-scrollbar {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
.scrollbar-md::-webkit-scrollbar-thumb {
|
||||
border: 3px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
/* scrollbar lg size */
|
||||
|
||||
.scrollbar-lg::-webkit-scrollbar {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
.scrollbar-lg::-webkit-scrollbar-thumb {
|
||||
border: 4px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
/* end scrollbar style */
|
||||
|
||||
/* progress bar */
|
||||
.progress-bar {
|
||||
fill: currentColor;
|
||||
color: rgba(var(--color-sidebar-background-100));
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder,
|
||||
::placeholder,
|
||||
:-ms-input-placeholder {
|
||||
color: rgb(var(--color-text-400));
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "@plane/typescript-config/vite-react.json",
|
||||
"compilerOptions": {
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/app/*": ["app/*"],
|
||||
"@/*": ["core/*"],
|
||||
"@/public/*": ["public/*"],
|
||||
"@/plane-admin/*": ["ce/*"],
|
||||
"@/styles/*": ["styles/*"]
|
||||
},
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": [
|
||||
"**/*",
|
||||
"**/.server/**/*",
|
||||
"**/.client/**/*",
|
||||
".react-router/types/**/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { reactRouter } from "@react-router/dev/vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import commonjs from "vite-plugin-commonjs";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), reactRouter(), tsconfigPaths(), commonjs()],
|
||||
define: {
|
||||
global: "globalThis",
|
||||
"process.env": JSON.stringify(process.env),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
process: "process/browser",
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
"process",
|
||||
"@plane/constants",
|
||||
"@plane/services",
|
||||
"@plane/types",
|
||||
"@plane/hooks",
|
||||
"@plane/ui",
|
||||
"@plane/utils",
|
||||
"@plane/i18n",
|
||||
],
|
||||
},
|
||||
build: {
|
||||
commonjsOptions: {
|
||||
include: [
|
||||
/node_modules/,
|
||||
/@plane\/constants/,
|
||||
/@plane\/services/,
|
||||
/@plane\/types/,
|
||||
/@plane\/hooks/,
|
||||
/@plane\/ui/,
|
||||
/@plane\/utils/,
|
||||
/@plane\/i18n/,
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -13,7 +13,7 @@
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/index.js",
|
||||
"import": "./dist/index.js"
|
||||
"import": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/index.js",
|
||||
"import": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/index.js",
|
||||
"import": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsdown --watch",
|
||||
"build": "tsdown",
|
||||
|
||||
@@ -16,36 +16,148 @@
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"exports": {
|
||||
"./accordion": "./dist/accordion/index.js",
|
||||
"./animated-counter": "./dist/animated-counter/index.js",
|
||||
"./avatar": "./dist/avatar/index.js",
|
||||
"./button": "./dist/button/index.js",
|
||||
"./calendar": "./dist/calendar/index.js",
|
||||
"./card": "./dist/card/index.js",
|
||||
"./charts/*": "./dist/charts/*/index.js",
|
||||
"./collapsible": "./dist/collapsible/index.js",
|
||||
"./combobox": "./dist/combobox/index.js",
|
||||
"./command": "./dist/command/index.js",
|
||||
"./context-menu": "./dist/context-menu/index.js",
|
||||
"./dialog": "./dist/dialog/index.js",
|
||||
"./emoji-icon-picker": "./dist/emoji-icon-picker/index.js",
|
||||
"./emoji-reaction": "./dist/emoji-reaction/index.js",
|
||||
"./emoji-reaction-picker": "./dist/emoji-reaction-picker/index.js",
|
||||
"./icons": "./dist/icons/index.js",
|
||||
"./menu": "./dist/menu/index.js",
|
||||
"./pill": "./dist/pill/index.js",
|
||||
"./popover": "./dist/popover/index.js",
|
||||
"./scrollarea": "./dist/scrollarea/index.js",
|
||||
"./skeleton": "./dist/skeleton/index.js",
|
||||
"./accordion": {
|
||||
"types": "./dist/accordion/index.d.ts",
|
||||
"require": "./dist/accordion/index.js",
|
||||
"import": "./dist/accordion/index.mjs"
|
||||
},
|
||||
"./animated-counter": {
|
||||
"types": "./dist/animated-counter/index.d.ts",
|
||||
"require": "./dist/animated-counter/index.js",
|
||||
"import": "./dist/animated-counter/index.mjs"
|
||||
},
|
||||
"./avatar": {
|
||||
"types": "./dist/avatar/index.d.ts",
|
||||
"require": "./dist/avatar/index.js",
|
||||
"import": "./dist/avatar/index.mjs"
|
||||
},
|
||||
"./button": {
|
||||
"types": "./dist/button/index.d.ts",
|
||||
"require": "./dist/button/index.js",
|
||||
"import": "./dist/button/index.mjs"
|
||||
},
|
||||
"./calendar": {
|
||||
"types": "./dist/calendar/index.d.ts",
|
||||
"require": "./dist/calendar/index.js",
|
||||
"import": "./dist/calendar/index.mjs"
|
||||
},
|
||||
"./card": {
|
||||
"types": "./dist/card/index.d.ts",
|
||||
"require": "./dist/card/index.js",
|
||||
"import": "./dist/card/index.mjs"
|
||||
},
|
||||
"./charts/*": {
|
||||
"types": "./dist/charts/*/index.d.ts",
|
||||
"require": "./dist/charts/*/index.js",
|
||||
"import": "./dist/charts/*/index.mjs"
|
||||
},
|
||||
"./collapsible": {
|
||||
"types": "./dist/collapsible/index.d.ts",
|
||||
"require": "./dist/collapsible/index.js",
|
||||
"import": "./dist/collapsible/index.mjs"
|
||||
},
|
||||
"./combobox": {
|
||||
"types": "./dist/combobox/index.d.ts",
|
||||
"require": "./dist/combobox/index.js",
|
||||
"import": "./dist/combobox/index.mjs"
|
||||
},
|
||||
"./command": {
|
||||
"types": "./dist/command/index.d.ts",
|
||||
"require": "./dist/command/index.js",
|
||||
"import": "./dist/command/index.mjs"
|
||||
},
|
||||
"./context-menu": {
|
||||
"types": "./dist/context-menu/index.d.ts",
|
||||
"require": "./dist/context-menu/index.js",
|
||||
"import": "./dist/context-menu/index.mjs"
|
||||
},
|
||||
"./dialog": {
|
||||
"types": "./dist/dialog/index.d.ts",
|
||||
"require": "./dist/dialog/index.js",
|
||||
"import": "./dist/dialog/index.mjs"
|
||||
},
|
||||
"./emoji-icon-picker": {
|
||||
"types": "./dist/emoji-icon-picker/index.d.ts",
|
||||
"require": "./dist/emoji-icon-picker/index.js",
|
||||
"import": "./dist/emoji-icon-picker/index.mjs"
|
||||
},
|
||||
"./emoji-reaction": {
|
||||
"types": "./dist/emoji-reaction/index.d.ts",
|
||||
"require": "./dist/emoji-reaction/index.js",
|
||||
"import": "./dist/emoji-reaction/index.mjs"
|
||||
},
|
||||
"./emoji-reaction-picker": {
|
||||
"types": "./dist/emoji-reaction-picker/index.d.ts",
|
||||
"require": "./dist/emoji-reaction-picker/index.js",
|
||||
"import": "./dist/emoji-reaction-picker/index.mjs"
|
||||
},
|
||||
"./icons": {
|
||||
"types": "./dist/icons/index.d.ts",
|
||||
"require": "./dist/icons/index.js",
|
||||
"import": "./dist/icons/index.mjs"
|
||||
},
|
||||
"./menu": {
|
||||
"types": "./dist/menu/index.d.ts",
|
||||
"require": "./dist/menu/index.js",
|
||||
"import": "./dist/menu/index.mjs"
|
||||
},
|
||||
"./pill": {
|
||||
"types": "./dist/pill/index.d.ts",
|
||||
"require": "./dist/pill/index.js",
|
||||
"import": "./dist/pill/index.mjs"
|
||||
},
|
||||
"./popover": {
|
||||
"types": "./dist/popover/index.d.ts",
|
||||
"require": "./dist/popover/index.js",
|
||||
"import": "./dist/popover/index.mjs"
|
||||
},
|
||||
"./scrollarea": {
|
||||
"types": "./dist/scrollarea/index.d.ts",
|
||||
"require": "./dist/scrollarea/index.js",
|
||||
"import": "./dist/scrollarea/index.mjs"
|
||||
},
|
||||
"./skeleton": {
|
||||
"types": "./dist/skeleton/index.d.ts",
|
||||
"require": "./dist/skeleton/index.js",
|
||||
"import": "./dist/skeleton/index.mjs"
|
||||
},
|
||||
"./styles/fonts": "./dist/styles/fonts/index.css",
|
||||
"./styles/react-day-picker": "./dist/styles/react-day-picker.css",
|
||||
"./switch": "./dist/switch/index.js",
|
||||
"./table": "./dist/table/index.js",
|
||||
"./tabs": "./dist/tabs/index.js",
|
||||
"./toast": "./dist/toast/index.js",
|
||||
"./toolbar": "./dist/toolbar/index.js",
|
||||
"./tooltip": "./dist/tooltip/index.js",
|
||||
"./utils": "./dist/utils/index.js"
|
||||
"./switch": {
|
||||
"types": "./dist/switch/index.d.ts",
|
||||
"require": "./dist/switch/index.js",
|
||||
"import": "./dist/switch/index.mjs"
|
||||
},
|
||||
"./table": {
|
||||
"types": "./dist/table/index.d.ts",
|
||||
"require": "./dist/table/index.js",
|
||||
"import": "./dist/table/index.mjs"
|
||||
},
|
||||
"./tabs": {
|
||||
"types": "./dist/tabs/index.d.ts",
|
||||
"require": "./dist/tabs/index.js",
|
||||
"import": "./dist/tabs/index.mjs"
|
||||
},
|
||||
"./toast": {
|
||||
"types": "./dist/toast/index.d.ts",
|
||||
"require": "./dist/toast/index.js",
|
||||
"import": "./dist/toast/index.mjs"
|
||||
},
|
||||
"./toolbar": {
|
||||
"types": "./dist/toolbar/index.d.ts",
|
||||
"require": "./dist/toolbar/index.js",
|
||||
"import": "./dist/toolbar/index.mjs"
|
||||
},
|
||||
"./tooltip": {
|
||||
"types": "./dist/tooltip/index.d.ts",
|
||||
"require": "./dist/tooltip/index.js",
|
||||
"import": "./dist/tooltip/index.mjs"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./dist/utils/index.d.ts",
|
||||
"require": "./dist/utils/index.js",
|
||||
"import": "./dist/utils/index.mjs"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui-components/react": "^1.0.0-beta.2",
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/index.js",
|
||||
"import": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/index.js",
|
||||
"import": "./dist/index.js"
|
||||
"import": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"types": ["node", "vite/client"],
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,13 @@
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/index.js",
|
||||
"import": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"dev": "tsdown --watch",
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/index.js",
|
||||
"import": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
|
||||