clean up ui pass 1

This commit is contained in:
miloschwartz 2025-06-30 09:33:48 -07:00
parent 3b6a44e683
commit a0381eb2c6
No known key found for this signature in database
82 changed files with 17618 additions and 17258 deletions

View file

@ -1136,5 +1136,13 @@
"initialSetupTitle": "Initial Server Setup",
"initialSetupDescription": "Create the intial server admin account. Only one server admin can exist. You can always change these credentials later.",
"createAdminAccount": "Create Admin Account",
"setupErrorCreateAdmin": "An error occurred while creating the server admin account."
"setupErrorCreateAdmin": "An error occurred while creating the server admin account.",
"documentation": "Documentation",
"saveAllSettings": "Save All Settings",
"settingsUpdated": "Settings updated",
"settingsUpdatedDescription": "All settings have been updated successfully",
"settingsErrorUpdate": "Failed to update settings",
"settingsErrorUpdateDescription": "An error occurred while updating settings",
"sidebarCollapse": "Collapse",
"sidebarExpand": "Expand"
}

245
package-lock.json generated
View file

@ -31,6 +31,7 @@
"@radix-ui/react-switch": "1.2.5",
"@radix-ui/react-tabs": "1.1.12",
"@radix-ui/react-toast": "1.2.14",
"@radix-ui/react-tooltip": "^1.2.7",
"@react-email/components": "0.0.41",
"@react-email/render": "^1.1.2",
"@react-email/tailwind": "1.0.5",
@ -960,6 +961,111 @@
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz",
"integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz",
"integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz",
"integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz",
"integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz",
"integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz",
"integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz",
"integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@noble/ciphers": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
@ -2028,6 +2134,40 @@
}
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz",
"integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.7",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-visually-hidden": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@ -14054,111 +14194,6 @@
"peerDependencies": {
"zod": "^3.24.4"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz",
"integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz",
"integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz",
"integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz",
"integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz",
"integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz",
"integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz",
"integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
}
}
}

View file

@ -49,6 +49,7 @@
"@radix-ui/react-switch": "1.2.5",
"@radix-ui/react-tabs": "1.1.12",
"@radix-ui/react-toast": "1.2.14",
"@radix-ui/react-tooltip": "^1.2.7",
"@react-email/components": "0.0.41",
"@react-email/render": "^1.1.2",
"@react-email/tailwind": "1.0.5",

View file

@ -95,7 +95,7 @@ export class Config {
? "true"
: "false";
process.env.FLAGS_ENABLE_CLIENTS = parsedConfig.flags?.disable_clients
process.env.FLAGS_ENABLE_CLIENTS = parsedConfig.flags?.enable_clients
? "true"
: "false";

View file

@ -41,7 +41,7 @@ import { createNewt, getNewtToken } from "./newt";
import { getOlmToken } from "./olm";
import rateLimit from "express-rate-limit";
import createHttpError from "http-errors";
import { verifyClientsEnabled } from "@server/middlewares/verifyClientsEnabled";
import { verifyClientsEnabled } from "@server/middlewares/verifyClintsEnabled";
// Root routes
export const unauthenticated = Router();

View file

@ -8,7 +8,6 @@ import { AxiosResponse } from "axios";
import { authCookieHeader } from "@app/lib/api/cookies";
import { redirect } from "next/navigation";
import { Layout } from "@app/components/Layout";
import { orgLangingNavItems, orgNavItems, rootNavItems } from "../navigation";
import { ListUserOrgsResponse } from "@server/routers/org";
type OrgPageProps = {
@ -60,7 +59,7 @@ export default async function OrgPage(props: OrgPageProps) {
return (
<UserProvider user={user}>
<Layout orgId={orgId} navItems={orgLangingNavItems} orgs={orgs}>
<Layout orgId={orgId} navItems={[]} orgs={orgs}>
{overview && (
<div className="w-full max-w-4xl mx-auto md:mt-32 mt-4">
<OrganizationLandingCard

View file

@ -18,6 +18,7 @@ import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
import moment from "moment";
export type InvitationRow = {
id: string;
@ -46,27 +47,44 @@ export default function InvitationsTable({
const { org } = useOrgContext();
const columns: ColumnDef<InvitationRow>[] = [
{
accessorKey: "email",
header: t("email")
},
{
accessorKey: "expiresAt",
header: t("expiresAt"),
cell: ({ row }) => {
const expiresAt = new Date(row.original.expiresAt);
const isExpired = expiresAt < new Date();
return (
<span className={isExpired ? "text-red-500" : ""}>
{moment(expiresAt).format("lll")}
</span>
);
}
},
{
accessorKey: "role",
header: t("role")
},
{
id: "dots",
cell: ({ row }) => {
const invitation = row.original;
return (
<div className="flex items-center justify-end gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">{t('openMenu')}</span>
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setIsRegenerateModalOpen(true);
setSelectedInvitation(invitation);
}}
>
<span>{t('inviteRegenerate')}</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setIsDeleteModalOpen(true);
@ -74,35 +92,24 @@ export default function InvitationsTable({
}}
>
<span className="text-red-500">
{t('inviteRemove')}
{t("inviteRemove")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
},
{
accessorKey: "email",
header: t('email')
},
{
accessorKey: "expiresAt",
header: t('expiresAt'),
cell: ({ row }) => {
const expiresAt = new Date(row.original.expiresAt);
const isExpired = expiresAt < new Date();
return (
<span className={isExpired ? "text-red-500" : ""}>
{expiresAt.toLocaleString()}
</span>
<Button
variant={"secondary"}
onClick={() => {
setIsRegenerateModalOpen(true);
setSelectedInvitation(invitation);
}}
>
<span>{t("inviteRegenerate")}</span>
</Button>
</div>
);
}
},
{
accessorKey: "role",
header: t('role')
}
];
@ -115,16 +122,18 @@ export default function InvitationsTable({
.catch((e) => {
toast({
variant: "destructive",
title: t('inviteRemoveError'),
description: t('inviteRemoveErrorDescription')
title: t("inviteRemoveError"),
description: t("inviteRemoveErrorDescription")
});
});
if (res && res.status === 200) {
toast({
variant: "default",
title: t('inviteRemoved'),
description: t('inviteRemovedDescription', {email: selectedInvitation.email})
title: t("inviteRemoved"),
description: t("inviteRemovedDescription", {
email: selectedInvitation.email
})
});
setInvitations((prev) =>
@ -148,20 +157,18 @@ export default function InvitationsTable({
dialog={
<div className="space-y-4">
<p>
{t('inviteQuestionRemove', {email: selectedInvitation?.email || ""})}
</p>
<p>
{t('inviteMessageRemove')}
</p>
<p>
{t('inviteMessageConfirm')}
{t("inviteQuestionRemove", {
email: selectedInvitation?.email || ""
})}
</p>
<p>{t("inviteMessageRemove")}</p>
<p>{t("inviteMessageConfirm")}</p>
</div>
}
buttonText={t('inviteRemoveConfirm')}
buttonText={t("inviteRemoveConfirm")}
onConfirm={removeInvitation}
string={selectedInvitation?.email ?? ""}
title={t('inviteRemove')}
title={t("inviteRemove")}
/>
<RegenerateInvitationForm
open={isRegenerateModalOpen}

View file

@ -201,7 +201,7 @@ export default function RegenerateInvitationForm({
setValidHours(parseInt(value))
}
>
<SelectTrigger>
<SelectTrigger className="w-full">
<SelectValue placeholder={t('inviteValidityPeriodSelect')} />
</SelectTrigger>
<SelectContent>

View file

@ -159,7 +159,6 @@ export default function DeleteRoleForm({
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className="space-y-4">
<div className="space-y-4">
<p>
{t('accessRoleQuestionRemove', {name: roleToDelete.name})}
@ -210,13 +209,13 @@ export default function DeleteRoleForm({
/>
</form>
</Form>
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
</CredenzaClose>
<Button
variant="destructive"
type="submit"
form="remove-role-form"
loading={loading}

View file

@ -19,7 +19,7 @@ import CreateRoleForm from "./CreateRoleForm";
import DeleteRoleForm from "./DeleteRoleForm";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from 'next-intl';
import { useTranslations } from "next-intl";
export type RoleRow = Role;
@ -42,49 +42,6 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
const t = useTranslations();
const columns: ColumnDef<RoleRow>[] = [
{
id: "actions",
cell: ({ row }) => {
const roleRow = row.original;
return (
<>
<div>
{roleRow.isAdmin && (
<MoreHorizontal className="h-4 w-4 opacity-0" />
)}
{!roleRow.isAdmin && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
{t('openMenu')}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setIsDeleteModalOpen(true);
setUserToRemove(roleRow);
}}
>
<span className="text-red-500">
{t('accessRoleDelete')}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</>
);
}
},
{
accessorKey: "name",
header: ({ column }) => {
@ -95,7 +52,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -103,7 +60,29 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
},
{
accessorKey: "description",
header: t('description')
header: t("description")
},
{
id: "actions",
cell: ({ row }) => {
const roleRow = row.original;
return (
<div className="flex items-center justify-end">
<Button
variant={"secondary"}
size="sm"
disabled={roleRow.isAdmin || false}
onClick={() => {
setIsDeleteModalOpen(true);
setUserToRemove(roleRow);
}}
>
{t("accessRoleDelete")}
</Button>
</div>
);
}
}
];

View file

@ -6,8 +6,6 @@ import { cache } from "react";
import OrgProvider from "@app/providers/OrgProvider";
import { ListRolesResponse } from "@server/routers/role";
import RolesTable, { RoleRow } from "./RolesTable";
import { SidebarSettings } from "@app/components/SidebarSettings";
import AccessPageHeaderAndNav from "../AccessPageHeaderAndNav";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { getTranslations } from 'next-intl/server';

View file

@ -20,7 +20,7 @@ import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useUserContext } from "@app/hooks/useUserContext";
import { useTranslations } from 'next-intl';
import { useTranslations } from "next-intl";
export type UserRow = {
id: string;
@ -51,65 +51,6 @@ export default function UsersTable({ users: u }: UsersTableProps) {
const t = useTranslations();
const columns: ColumnDef<UserRow>[] = [
{
id: "dots",
cell: ({ row }) => {
const userRow = row.original;
return (
<>
<div>
{userRow.isOwner && (
<MoreHorizontal className="h-4 w-4 opacity-0" />
)}
{!userRow.isOwner && (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
{t('openMenu')}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
className="block w-full"
>
<DropdownMenuItem>
{t('accessUsersManage')}
</DropdownMenuItem>
</Link>
{`${userRow.username}-${userRow.idpId}` !==
`${user?.username}-${userRow.idpId}` && (
<DropdownMenuItem
onClick={() => {
setIsDeleteModalOpen(
true
);
setSelectedUser(
userRow
);
}}
>
<span className="text-red-500">
{t('accessUserRemove')}
</span>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</div>
</>
);
}
},
{
accessorKey: "displayUsername",
header: ({ column }) => {
@ -120,7 +61,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('username')}
{t("username")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -136,7 +77,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('identityProvider')}
{t("identityProvider")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -152,7 +93,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('role')}
{t("role")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -176,12 +117,68 @@ export default function UsersTable({ users: u }: UsersTableProps) {
const userRow = row.original;
return (
<div className="flex items-center justify-end">
<>
<div>
{userRow.isOwner && (
<MoreHorizontal className="h-4 w-4 opacity-0" />
)}
{!userRow.isOwner && (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="opacity-0 cursor-default"
className="h-8 w-8 p-0"
>
{t('placeholder')}
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
className="block w-full"
>
<DropdownMenuItem>
{t("accessUsersManage")}
</DropdownMenuItem>
</Link>
{`${userRow.username}-${userRow.idpId}` !==
`${user?.username}-${userRow.idpId}` && (
<DropdownMenuItem
onClick={() => {
setIsDeleteModalOpen(
true
);
setSelectedUser(
userRow
);
}}
>
<span className="text-red-500">
{t(
"accessUserRemove"
)}
</span>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</div>
</>
{userRow.isOwner && (
<Button
variant={"secondary"}
className="ml-2"
size="sm"
disabled={true}
>
{t("manage")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
)}
{!userRow.isOwner && (
@ -189,10 +186,12 @@ export default function UsersTable({ users: u }: UsersTableProps) {
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
>
<Button
variant={"outlinePrimary"}
variant={"secondary"}
className="ml-2"
size="sm"
disabled={userRow.isOwner}
>
{t('manage')}
{t("manage")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@ -210,10 +209,10 @@ export default function UsersTable({ users: u }: UsersTableProps) {
.catch((e) => {
toast({
variant: "destructive",
title: t('userErrorOrgRemove'),
title: t("userErrorOrgRemove"),
description: formatAxiosError(
e,
t('userErrorOrgRemoveDescription')
t("userErrorOrgRemoveDescription")
)
});
});
@ -221,8 +220,10 @@ export default function UsersTable({ users: u }: UsersTableProps) {
if (res && res.status === 200) {
toast({
variant: "default",
title: t('userOrgRemoved'),
description: t('userOrgRemovedDescription', {email: selectedUser.email || ""})
title: t("userOrgRemoved"),
description: t("userOrgRemovedDescription", {
email: selectedUser.email || ""
})
});
setUsers((prev) =>
@ -244,19 +245,21 @@ export default function UsersTable({ users: u }: UsersTableProps) {
dialog={
<div className="space-y-4">
<p>
{t('userQuestionOrgRemove', {email: selectedUser?.email || selectedUser?.name || selectedUser?.username || ""})}
{t("userQuestionOrgRemove", {
email:
selectedUser?.email ||
selectedUser?.name ||
selectedUser?.username ||
""
})}
</p>
<p>
{t('userMessageOrgRemove')}
</p>
<p>{t("userMessageOrgRemove")}</p>
<p>
{t('userMessageOrgConfirm')}
</p>
<p>{t("userMessageOrgConfirm")}</p>
</div>
}
buttonText={t('userRemoveOrgConfirm')}
buttonText={t("userRemoveOrgConfirm")}
onConfirm={removeUser}
string={
selectedUser?.email ||
@ -264,14 +267,16 @@ export default function UsersTable({ users: u }: UsersTableProps) {
selectedUser?.username ||
""
}
title={t('userRemoveOrg')}
title={t("userRemoveOrg")}
/>
<UsersDataTable
columns={columns}
data={users}
inviteUser={() => {
router.push(`/${org?.org.orgId}/settings/access/users/create`);
router.push(
`/${org?.org.orgId}/settings/access/users/create`
);
}}
/>
</>

View file

@ -5,17 +5,9 @@ import { authCookieHeader } from "@app/lib/api/cookies";
import { GetOrgUserResponse } from "@server/routers/user";
import OrgUserProvider from "@app/providers/OrgUserProvider";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import Link from "next/link";
import { cache } from "react";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { getTranslations } from 'next-intl/server';
import { getTranslations } from "next-intl/server";
interface UserLayoutProps {
children: React.ReactNode;
@ -45,7 +37,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
const navItems = [
{
title: t('accessControls'),
title: t("accessControls"),
href: "/{orgId}/settings/access/users/{userId}/access-controls"
}
];
@ -54,12 +46,10 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
<>
<SettingsSectionTitle
title={`${user?.email}`}
description={t('userDescription2')}
description={t("userDescription2")}
/>
<OrgUserProvider orgUser={user}>
<HorizontalTabs items={navItems}>
{children}
</HorizontalTabs>
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
</OrgUserProvider>
</>
);

View file

@ -78,40 +78,42 @@ export default function Page() {
const [dataLoaded, setDataLoaded] = useState(false);
const internalFormSchema = z.object({
email: z.string().email({ message: t('emailInvalid') }),
validForHours: z.string().min(1, { message: t('inviteValidityDuration') }),
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') })
email: z.string().email({ message: t("emailInvalid") }),
validForHours: z
.string()
.min(1, { message: t("inviteValidityDuration") }),
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") })
});
const externalFormSchema = z.object({
username: z.string().min(1, { message: t('usernameRequired') }),
username: z.string().min(1, { message: t("usernameRequired") }),
email: z
.string()
.email({ message: t('emailInvalid') })
.email({ message: t("emailInvalid") })
.optional()
.or(z.literal("")),
name: z.string().optional(),
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') }),
idpId: z.string().min(1, { message: t('idpSelectPlease') })
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") }),
idpId: z.string().min(1, { message: t("idpSelectPlease") })
});
const formatIdpType = (type: string) => {
switch (type.toLowerCase()) {
case "oidc":
return t('idpGenericOidc');
return t("idpGenericOidc");
default:
return type;
}
};
const validFor = [
{ hours: 24, name: t('day', {count: 1}) },
{ hours: 48, name: t('day', {count: 2}) },
{ hours: 72, name: t('day', {count: 3}) },
{ hours: 96, name: t('day', {count: 4}) },
{ hours: 120, name: t('day', {count: 5}) },
{ hours: 144, name: t('day', {count: 6}) },
{ hours: 168, name: t('day', {count: 7}) }
{ hours: 24, name: t("day", { count: 1 }) },
{ hours: 48, name: t("day", { count: 2 }) },
{ hours: 72, name: t("day", { count: 3 }) },
{ hours: 96, name: t("day", { count: 4 }) },
{ hours: 120, name: t("day", { count: 5 }) },
{ hours: 144, name: t("day", { count: 6 }) },
{ hours: 168, name: t("day", { count: 7 }) }
];
const internalForm = useForm<z.infer<typeof internalFormSchema>>({
@ -157,10 +159,10 @@ export default function Page() {
console.error(e);
toast({
variant: "destructive",
title: t('accessRoleErrorFetch'),
title: t("accessRoleErrorFetch"),
description: formatAxiosError(
e,
t('accessRoleErrorFetchDescription')
t("accessRoleErrorFetchDescription")
)
});
});
@ -180,10 +182,10 @@ export default function Page() {
console.error(e);
toast({
variant: "destructive",
title: t('idpErrorFetch'),
title: t("idpErrorFetch"),
description: formatAxiosError(
e,
t('idpErrorFetchDescription')
t("idpErrorFetchDescription")
)
});
});
@ -220,16 +222,16 @@ export default function Page() {
if (e.response?.status === 409) {
toast({
variant: "destructive",
title: t('userErrorExists'),
description: t('userErrorExistsDescription')
title: t("userErrorExists"),
description: t("userErrorExistsDescription")
});
} else {
toast({
variant: "destructive",
title: t('inviteError'),
title: t("inviteError"),
description: formatAxiosError(
e,
t('inviteErrorDescription')
t("inviteErrorDescription")
)
});
}
@ -239,8 +241,8 @@ export default function Page() {
setInviteLink(res.data.data.inviteLink);
toast({
variant: "default",
title: t('userInvited'),
description: t('userInvitedDescription')
title: t("userInvited"),
description: t("userInvitedDescription")
});
setExpiresInDays(parseInt(values.validForHours) / 24);
@ -266,10 +268,10 @@ export default function Page() {
.catch((e) => {
toast({
variant: "destructive",
title: t('userErrorCreate'),
title: t("userErrorCreate"),
description: formatAxiosError(
e,
t('userErrorCreateDescription')
t("userErrorCreateDescription")
)
});
});
@ -277,8 +279,8 @@ export default function Page() {
if (res && res.status === 201) {
toast({
variant: "default",
title: t('userCreated'),
description: t('userCreatedDescription')
title: t("userCreated"),
description: t("userCreatedDescription")
});
router.push(`/${orgId}/settings/access/users`);
}
@ -289,13 +291,13 @@ export default function Page() {
const userTypes: ReadonlyArray<UserTypeOption> = [
{
id: "internal",
title: t('userTypeInternal'),
description: t('userTypeInternalDescription')
title: t("userTypeInternal"),
description: t("userTypeInternalDescription")
},
{
id: "oidc",
title: t('userTypeExternal'),
description: t('userTypeExternalDescription')
title: t("userTypeExternal"),
description: t("userTypeExternalDescription")
}
];
@ -303,8 +305,8 @@ export default function Page() {
<>
<div className="flex justify-between">
<HeaderTitle
title={t('accessUserCreate')}
description={t('accessUserCreateDescription')}
title={t("accessUserCreate")}
description={t("accessUserCreateDescription")}
/>
<Button
variant="outline"
@ -312,19 +314,20 @@ export default function Page() {
router.push(`/${orgId}/settings/access/users`);
}}
>
{t('userSeeAll')}
{t("userSeeAll")}
</Button>
</div>
<div>
<SettingsContainer>
{!inviteLink && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('userTypeTitle')}
{t("userTypeTitle")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('userTypeDescription')}
{t("userTypeDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@ -344,16 +347,18 @@ export default function Page() {
/>
</SettingsSectionBody>
</SettingsSection>
)}
{userType === "internal" && dataLoaded && (
<>
{!inviteLink ? (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('userSettings')}
{t("userSettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('userSettingsDescription')}
{t("userSettingsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@ -374,7 +379,7 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('email')}
{t("email")}
</FormLabel>
<FormControl>
<Input
@ -386,28 +391,6 @@ export default function Page() {
)}
/>
{env.email.emailEnabled && (
<div className="flex items-center space-x-2">
<Checkbox
id="send-email"
checked={sendEmail}
onCheckedChange={(
e
) =>
setSendEmail(
e as boolean
)
}
/>
<label
htmlFor="send-email"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t('inviteEmailSent')}
</label>
</div>
)}
<FormField
control={
internalForm.control
@ -416,7 +399,9 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('inviteValid')}
{t(
"inviteValid"
)}
</FormLabel>
<Select
onValueChange={
@ -427,8 +412,12 @@ export default function Page() {
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('selectDuration')} />
<SelectTrigger className="w-full">
<SelectValue
placeholder={t(
"selectDuration"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
@ -463,7 +452,7 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('role')}
{t("role")}
</FormLabel>
<Select
onValueChange={
@ -471,8 +460,12 @@ export default function Page() {
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('accessRoleSelect')} />
<SelectTrigger className="w-full">
<SelectValue
placeholder={t(
"accessRoleSelect"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
@ -499,25 +492,27 @@ export default function Page() {
)}
/>
{inviteLink && (
<div className="max-w-md space-y-4">
{sendEmail && (
<p>
{t('inviteEmailSentDescription')}
</p>
)}
{!sendEmail && (
<p>
{t('inviteSentDescription')}
</p>
)}
<p>
{t('inviteExpiresIn', {days: expiresInDays})}
</p>
<CopyTextBox
text={inviteLink}
wrapText={false}
{env.email.emailEnabled && (
<div className="flex items-center space-x-2">
<Checkbox
id="send-email"
checked={sendEmail}
onCheckedChange={(
e
) =>
setSendEmail(
e as boolean
)
}
/>
<label
htmlFor="send-email"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t(
"inviteEmailSent"
)}
</label>
</div>
)}
</form>
@ -525,6 +520,33 @@ export default function Page() {
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
) : (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("userInvited")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{sendEmail
? t("inviteEmailSentDescription")
: t("inviteSentDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<div className="space-y-4">
<p>
{t("inviteExpiresIn", {
days: expiresInDays
})}
</p>
<CopyTextBox
text={inviteLink}
wrapText={false}
/>
</div>
</SettingsSectionBody>
</SettingsSection>
)}
</>
)}
@ -533,16 +555,16 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('idpTitle')}
{t("idpTitle")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('idpSelect')}
{t("idpSelect")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
{idps.length === 0 ? (
<p className="text-muted-foreground">
{t('idpNotConfigured')}
{t("idpNotConfigured")}
</p>
) : (
<Form {...externalForm}>
@ -596,10 +618,10 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('userSettings')}
{t("userSettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('userSettingsDescription')}
{t("userSettingsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@ -620,7 +642,9 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('username')}
{t(
"username"
)}
</FormLabel>
<FormControl>
<Input
@ -628,7 +652,9 @@ export default function Page() {
/>
</FormControl>
<p className="text-sm text-muted-foreground mt-1">
{t('usernameUniq')}
{t(
"usernameUniq"
)}
</p>
<FormMessage />
</FormItem>
@ -643,7 +669,9 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('emailOptional')}
{t(
"emailOptional"
)}
</FormLabel>
<FormControl>
<Input
@ -663,7 +691,9 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('nameOptional')}
{t(
"nameOptional"
)}
</FormLabel>
<FormControl>
<Input
@ -683,7 +713,7 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('role')}
{t("role")}
</FormLabel>
<Select
onValueChange={
@ -691,8 +721,12 @@ export default function Page() {
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('accessRoleSelect')} />
<SelectTrigger className="w-full">
<SelectValue
placeholder={t(
"accessRoleSelect"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
@ -736,19 +770,17 @@ export default function Page() {
router.push(`/${orgId}/settings/access/users`);
}}
>
{t('cancel')}
{t("cancel")}
</Button>
{userType && dataLoaded && (
<Button
type="submit"
form="create-user-form"
type={inviteLink ? "button" : "submit"}
form={inviteLink ? undefined : "create-user-form"}
loading={loading}
disabled={
loading ||
(userType === "internal" && inviteLink !== null)
}
disabled={loading}
onClick={inviteLink ? () => router.push(`/${orgId}/settings/access/users`) : undefined}
>
{t('accessUserCreate')}
{inviteLink ? t("done") : t("accessUserCreate")}
</Button>
)}
</div>

View file

@ -50,11 +50,14 @@ export default function OrgApiKeysTable({
const deleteSite = (apiKeyId: string) => {
api.delete(`/org/${orgId}/api-key/${apiKeyId}`)
.catch((e) => {
console.error(t('apiKeysErrorDelete'), e);
console.error(t("apiKeysErrorDelete"), e);
toast({
variant: "destructive",
title: t('apiKeysErrorDelete'),
description: formatAxiosError(e, t('apiKeysErrorDeleteMessage'))
title: t("apiKeysErrorDelete"),
description: formatAxiosError(
e,
t("apiKeysErrorDeleteMessage")
)
});
})
.then(() => {
@ -68,41 +71,6 @@ export default function OrgApiKeysTable({
};
const columns: ColumnDef<OrgApiKeyRow>[] = [
{
id: "dots",
cell: ({ row }) => {
const apiKeyROw = row.original;
const router = useRouter();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">{t('openMenu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setSelected(apiKeyROw);
}}
>
<span>{t('viewSettings')}</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSelected(apiKeyROw);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
},
{
accessorKey: "name",
header: ({ column }) => {
@ -113,7 +81,7 @@ export default function OrgApiKeysTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -121,7 +89,7 @@ export default function OrgApiKeysTable({
},
{
accessorKey: "key",
header: t('key'),
header: t("key"),
cell: ({ row }) => {
const r = row.original;
return <span className="font-mono">{r.key}</span>;
@ -129,7 +97,7 @@ export default function OrgApiKeysTable({
},
{
accessorKey: "createdAt",
header: t('createdAt'),
header: t("createdAt"),
cell: ({ row }) => {
const r = row.original;
return <span>{moment(r.createdAt).format("lll")}</span>;
@ -141,9 +109,43 @@ export default function OrgApiKeysTable({
const r = row.original;
return (
<div className="flex items-center justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setSelected(r);
}}
>
<span>{t("viewSettings")}</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSelected(r);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Link href={`/${orgId}/settings/api-keys/${r.id}`}>
<Button variant={"outlinePrimary"} className="ml-2">
{t('edit')}
<Button
variant={"secondary"}
className="ml-2"
size="sm"
>
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@ -165,24 +167,23 @@ export default function OrgApiKeysTable({
dialog={
<div className="space-y-4">
<p>
{t('apiKeysQuestionRemove', {selectedApiKey: selected?.name || selected?.id})}
{t("apiKeysQuestionRemove", {
selectedApiKey:
selected?.name || selected?.id
})}
</p>
<p>
<b>
{t('apiKeysMessageRemove')}
</b>
<b>{t("apiKeysMessageRemove")}</b>
</p>
<p>
{t('apiKeysMessageConfirm')}
</p>
<p>{t("apiKeysMessageConfirm")}</p>
</div>
}
buttonText={t('apiKeysDeleteConfirm')}
buttonText={t("apiKeysDeleteConfirm")}
onConfirm={async () => deleteSite(selected!.id)}
string={selected.name}
title={t('apiKeysDelete')}
title={t("apiKeysDelete")}
/>
)}

View file

@ -2,16 +2,7 @@ import { internal } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { authCookieHeader } from "@app/lib/api/cookies";
import { SidebarSettings } from "@app/components/SidebarSettings";
import Link from "next/link";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import { GetApiKeyResponse } from "@server/routers/apiKeys";
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
import { HorizontalTabs } from "@app/components/HorizontalTabs";

View file

@ -25,21 +25,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@app/components/ui/input";
import { InfoIcon } from "lucide-react";
import { Button } from "@app/components/ui/button";
import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios";
import { useParams, useRouter } from "next/navigation";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import Link from "next/link";
import {
CreateOrgApiKeyBody,
CreateOrgApiKeyResponse
@ -110,7 +101,7 @@ export default function Page() {
const copiedForm = useForm<CopiedFormValues>({
resolver: zodResolver(copiedFormSchema),
defaultValues: {
copied: false
copied: true
}
});
@ -308,54 +299,54 @@ export default function Page() {
</AlertDescription>
</Alert>
<h4 className="font-semibold">
{t('apiKeysInfo')}
</h4>
{/* <h4 className="font-semibold"> */}
{/* {t('apiKeysInfo')} */}
{/* </h4> */}
<CopyTextBox
text={`${apiKey.apiKeyId}.${apiKey.apiKey}`}
/>
<Form {...copiedForm}>
<form
className="space-y-4"
id="copied-form"
>
<FormField
control={copiedForm.control}
name="copied"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
defaultChecked={
copiedForm.getValues(
"copied"
) as boolean
}
onCheckedChange={(
e
) => {
copiedForm.setValue(
"copied",
e as boolean
);
}}
/>
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t('apiKeysConfirmCopy')}
</label>
</div>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
{/* <Form {...copiedForm}> */}
{/* <form */}
{/* className="space-y-4" */}
{/* id="copied-form" */}
{/* > */}
{/* <FormField */}
{/* control={copiedForm.control} */}
{/* name="copied" */}
{/* render={({ field }) => ( */}
{/* <FormItem> */}
{/* <div className="flex items-center space-x-2"> */}
{/* <Checkbox */}
{/* id="terms" */}
{/* defaultChecked={ */}
{/* copiedForm.getValues( */}
{/* "copied" */}
{/* ) as boolean */}
{/* } */}
{/* onCheckedChange={( */}
{/* e */}
{/* ) => { */}
{/* copiedForm.setValue( */}
{/* "copied", */}
{/* e as boolean */}
{/* ); */}
{/* }} */}
{/* /> */}
{/* <label */}
{/* htmlFor="terms" */}
{/* className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" */}
{/* > */}
{/* {t('apiKeysConfirmCopy')} */}
{/* </label> */}
{/* </div> */}
{/* <FormMessage /> */}
{/* </FormItem> */}
{/* )} */}
{/* /> */}
{/* </form> */}
{/* </Form> */}
</SettingsSectionBody>
</SettingsSection>
)}

View file

@ -246,7 +246,7 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
<Link
href={`/${clientRow.orgId}/settings/clients/${clientRow.id}`}
>
<Button variant={"outlinePrimary"} className="ml-2">
<Button variant={"secondary"} className="ml-2">
Edit
<ArrowRight className="ml-2 w-4 h-4" />
</Button>

View file

@ -1,7 +1,6 @@
import { internal } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { authCookieHeader } from "@app/lib/api/cookies";
import { SidebarSettings } from "@app/components/SidebarSettings";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { GetClientResponse } from "@server/routers/client";
import ClientInfoCard from "./ClientInfoCard";

View file

@ -18,9 +18,9 @@ import { GetOrgUserResponse } from "@server/routers/user";
import UserProvider from "@app/providers/UserProvider";
import { Layout } from "@app/components/Layout";
import { SidebarNavItem, SidebarNavProps } from "@app/components/SidebarNav";
import { orgNavItems } from "@app/app/navigation";
import { getTranslations } from "next-intl/server";
import { pullEnv } from "@app/lib/pullEnv";
import { orgNavSections } from "@app/app/navigation";
export const dynamic = "force-dynamic";
@ -82,24 +82,9 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
}
} catch (e) {}
if (env.flags.enableClients) {
const existing = orgNavItems.find(
(item) => item.title === "sidebarClients"
);
if (!existing) {
const clientsNavItem = {
title: "sidebarClients",
href: "/{orgId}/settings/clients",
icon: <Workflow className="h-4 w-4" />
};
orgNavItems.splice(1, 0, clientsNavItem);
}
}
return (
<UserProvider user={user}>
<Layout orgId={params.orgId} orgs={orgs} navItems={orgNavItems}>
<Layout orgId={params.orgId} orgs={orgs} navItems={orgNavSections}>
{children}
</Layout>
</UserProvider>

View file

@ -31,7 +31,7 @@ import CopyToClipboard from "@app/components/CopyToClipboard";
import { Switch } from "@app/components/ui/switch";
import { AxiosResponse } from "axios";
import { UpdateResourceResponse } from "@server/routers/resource";
import { useTranslations } from 'next-intl';
import { useTranslations } from "next-intl";
export type ResourceRow = {
id: number;
@ -65,11 +65,11 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
const deleteResource = (resourceId: number) => {
api.delete(`/resource/${resourceId}`)
.catch((e) => {
console.error(t('resourceErrorDelte'), e);
console.error(t("resourceErrorDelte"), e);
toast({
variant: "destructive",
title: t('resourceErrorDelte'),
description: formatAxiosError(e, t('resourceErrorDelte'))
title: t("resourceErrorDelte"),
description: formatAxiosError(e, t("resourceErrorDelte"))
});
})
.then(() => {
@ -89,50 +89,16 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
.catch((e) => {
toast({
variant: "destructive",
title: t('resourcesErrorUpdate'),
description: formatAxiosError(e, t('resourcesErrorUpdateDescription'))
title: t("resourcesErrorUpdate"),
description: formatAxiosError(
e,
t("resourcesErrorUpdateDescription")
)
});
});
}
const columns: ColumnDef<ResourceRow>[] = [
{
accessorKey: "dots",
header: "",
cell: ({ row }) => {
const resourceRow = row.original;
const router = useRouter();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">{t('openMenu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
>
<DropdownMenuItem>
{t('viewSettings')}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setSelectedResource(resourceRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
},
{
accessorKey: "name",
header: ({ column }) => {
@ -143,7 +109,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -159,7 +125,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('site')}
{t("site")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -170,7 +136,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
<Link
href={`/${resourceRow.orgId}/settings/sites/${resourceRow.siteId}`}
>
<Button variant="outline">
<Button variant="outline" size="sm">
{resourceRow.site}
<ArrowUpRight className="ml-2 h-4 w-4" />
</Button>
@ -180,7 +146,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
},
{
accessorKey: "protocol",
header: t('protocol'),
header: t("protocol"),
cell: ({ row }) => {
const resourceRow = row.original;
return <span>{resourceRow.protocol.toUpperCase()}</span>;
@ -188,7 +154,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
},
{
accessorKey: "domain",
header: t('access'),
header: t("access"),
cell: ({ row }) => {
const resourceRow = row.original;
return (
@ -218,7 +184,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('authentication')}
{t("authentication")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -230,12 +196,12 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
{resourceRow.authState === "protected" ? (
<span className="text-green-500 flex items-center space-x-2">
<ShieldCheck className="w-4 h-4" />
<span>{t('protected')}</span>
<span>{t("protected")}</span>
</span>
) : resourceRow.authState === "not_protected" ? (
<span className="text-yellow-500 flex items-center space-x-2">
<ShieldOff className="w-4 h-4" />
<span>{t('notProtected')}</span>
<span>{t("notProtected")}</span>
</span>
) : (
<span>-</span>
@ -246,7 +212,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
},
{
accessorKey: "enabled",
header: t('enabled'),
header: t("enabled"),
cell: ({ row }) => (
<Switch
defaultChecked={row.original.enabled}
@ -262,11 +228,41 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
const resourceRow = row.original;
return (
<div className="flex items-center justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
>
<DropdownMenuItem>
{t("viewSettings")}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setSelectedResource(resourceRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Link
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
>
<Button variant={"outlinePrimary"} className="ml-2">
{t('edit')}
<Button variant={"secondary"} className="ml-2" size="sm">
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@ -288,22 +284,22 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
dialog={
<div>
<p className="mb-2">
{t('resourceQuestionRemove', {selectedResource: selectedResource?.name || selectedResource?.id})}
{t("resourceQuestionRemove", {
selectedResource:
selectedResource?.name ||
selectedResource?.id
})}
</p>
<p className="mb-2">
{t('resourceMessageRemove')}
</p>
<p className="mb-2">{t("resourceMessageRemove")}</p>
<p>
{t('resourceMessageConfirm')}
</p>
<p>{t("resourceMessageConfirm")}</p>
</div>
}
buttonText={t('resourceDeleteConfirm')}
buttonText={t("resourceDeleteConfirm")}
onConfirm={async () => deleteResource(selectedResource!.id)}
string={selectedResource.name}
title={t('resourceDelete')}
title={t("resourceDelete")}
/>
)}

View file

@ -568,7 +568,7 @@ export default function ResourceAuthenticationPage() {
</span>
</div>
<Button
variant="outlinePrimary"
variant="secondary"
onClick={
authInfo.password
? removeResourcePassword
@ -593,7 +593,7 @@ export default function ResourceAuthenticationPage() {
</span>
</div>
<Button
variant="outlinePrimary"
variant="secondary"
onClick={
authInfo.pincode
? removeResourcePincode

View file

@ -128,7 +128,7 @@ export default function ReverseProxyTargets(props: {
return true;
},
{
message: t('proxyErrorInvalidHeader')
message: t("proxyErrorInvalidHeader")
}
)
});
@ -146,7 +146,7 @@ export default function ReverseProxyTargets(props: {
return true;
},
{
message: t('proxyErrorTls')
message: t("proxyErrorTls")
}
)
});
@ -203,10 +203,10 @@ export default function ReverseProxyTargets(props: {
console.error(err);
toast({
variant: "destructive",
title: t('targetErrorFetch'),
title: t("targetErrorFetch"),
description: formatAxiosError(
err,
t('targetErrorFetchDescription')
t("targetErrorFetchDescription")
)
});
} finally {
@ -228,10 +228,10 @@ export default function ReverseProxyTargets(props: {
console.error(err);
toast({
variant: "destructive",
title: t('siteErrorFetch'),
title: t("siteErrorFetch"),
description: formatAxiosError(
err,
t('siteErrorFetchDescription')
t("siteErrorFetchDescription")
)
});
}
@ -251,8 +251,8 @@ export default function ReverseProxyTargets(props: {
if (isDuplicate) {
toast({
variant: "destructive",
title: t('targetErrorDuplicate'),
description: t('targetErrorDuplicateDescription')
title: t("targetErrorDuplicate"),
description: t("targetErrorDuplicateDescription")
});
return;
}
@ -264,8 +264,8 @@ export default function ReverseProxyTargets(props: {
if (!isIPInSubnet(targetIp, subnet)) {
toast({
variant: "destructive",
title: t('targetWireGuardErrorInvalidIp'),
description: t('targetWireGuardErrorInvalidIpDescription')
title: t("targetWireGuardErrorInvalidIp"),
description: t("targetWireGuardErrorInvalidIpDescription")
});
return;
}
@ -307,10 +307,13 @@ export default function ReverseProxyTargets(props: {
);
}
async function saveTargets() {
async function saveAllSettings() {
try {
setTargetsLoading(true);
setHttpsTlsLoading(true);
setProxySettingsLoading(true);
// Save targets
for (let target of targets) {
const data = {
ip: target.ip,
@ -342,9 +345,31 @@ export default function ReverseProxyTargets(props: {
});
updateResource({ stickySession: stickySessionData.stickySession });
// Save TLS settings
const tlsData = tlsSettingsForm.getValues();
await api.post(`/resource/${params.resourceId}`, {
ssl: tlsData.ssl,
tlsServerName: tlsData.tlsServerName || null
});
updateResource({
...resource,
ssl: tlsData.ssl,
tlsServerName: tlsData.tlsServerName || null
});
// Save proxy settings
const proxyData = proxySettingsForm.getValues();
await api.post(`/resource/${params.resourceId}`, {
setHostHeader: proxyData.setHostHeader || null
});
updateResource({
...resource,
setHostHeader: proxyData.setHostHeader || null
});
toast({
title: t('targetsUpdated'),
description: t('targetsUpdatedDescription')
title: t("settingsUpdated"),
description: t("settingsUpdatedDescription")
});
setTargetsToRemove([]);
@ -353,73 +378,15 @@ export default function ReverseProxyTargets(props: {
console.error(err);
toast({
variant: "destructive",
title: t('targetsErrorUpdate'),
title: t("settingsErrorUpdate"),
description: formatAxiosError(
err,
t('targetsErrorUpdateDescription')
t("settingsErrorUpdateDescription")
)
});
} finally {
setTargetsLoading(false);
}
}
async function saveTlsSettings(data: TlsSettingsValues) {
try {
setHttpsTlsLoading(true);
await api.post(`/resource/${params.resourceId}`, {
ssl: data.ssl,
tlsServerName: data.tlsServerName || null
});
updateResource({
...resource,
ssl: data.ssl,
tlsServerName: data.tlsServerName || null
});
toast({
title: t('targetTlsUpdate'),
description: t('targetTlsUpdateDescription')
});
} catch (err) {
console.error(err);
toast({
variant: "destructive",
title: t('targetErrorTlsUpdate'),
description: formatAxiosError(
err,
t('targetErrorTlsUpdateDescription')
)
});
} finally {
setHttpsTlsLoading(false);
}
}
async function saveProxySettings(data: ProxySettingsValues) {
try {
setProxySettingsLoading(true);
await api.post(`/resource/${params.resourceId}`, {
setHostHeader: data.setHostHeader || null
});
updateResource({
...resource,
setHostHeader: data.setHostHeader || null
});
toast({
title: t('proxyUpdated'),
description: t('proxyUpdatedDescription')
});
} catch (err) {
console.error(err);
toast({
variant: "destructive",
title: t('proxyErrorUpdate'),
description: formatAxiosError(
err,
t('proxyErrorUpdateDescription')
)
});
} finally {
setProxySettingsLoading(false);
}
}
@ -427,7 +394,7 @@ export default function ReverseProxyTargets(props: {
const columns: ColumnDef<LocalTarget>[] = [
{
accessorKey: "ip",
header: t('targetAddr'),
header: t("targetAddr"),
cell: ({ row }) => (
<Input
defaultValue={row.original.ip}
@ -442,7 +409,7 @@ export default function ReverseProxyTargets(props: {
},
{
accessorKey: "port",
header: t('targetPort'),
header: t("targetPort"),
cell: ({ row }) => (
<Input
type="number"
@ -476,7 +443,7 @@ export default function ReverseProxyTargets(props: {
// },
{
accessorKey: "enabled",
header: t('enabled'),
header: t("enabled"),
cell: ({ row }) => (
<Switch
defaultChecked={row.original.enabled}
@ -503,7 +470,7 @@ export default function ReverseProxyTargets(props: {
variant="outline"
onClick={() => removeTarget(row.original.targetId)}
>
{t('delete')}
{t("delete")}
</Button>
</div>
</>
@ -514,7 +481,7 @@ export default function ReverseProxyTargets(props: {
if (resource.http) {
const methodCol: ColumnDef<LocalTarget> = {
accessorKey: "method",
header: t('method'),
header: t("method"),
cell: ({ row }) => (
<Select
defaultValue={row.original.method ?? ""}
@ -561,11 +528,9 @@ export default function ReverseProxyTargets(props: {
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('targets')}
</SettingsSectionTitle>
<SettingsSectionTitle>{t("targets")}</SettingsSectionTitle>
<SettingsSectionDescription>
{t('targetsDescription')}
{t("targetsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@ -573,7 +538,7 @@ export default function ReverseProxyTargets(props: {
<Form {...targetsSettingsForm}>
<form
onSubmit={targetsSettingsForm.handleSubmit(
saveTargets
saveAllSettings
)}
className="space-y-4"
id="targets-settings-form"
@ -587,8 +552,12 @@ export default function ReverseProxyTargets(props: {
<FormControl>
<SwitchInput
id="sticky-toggle"
label={t('targetStickySessions')}
description={t('targetStickySessionsDescription')}
label={t(
"targetStickySessions"
)}
description={t(
"targetStickySessionsDescription"
)}
defaultChecked={
field.value
}
@ -619,7 +588,9 @@ export default function ReverseProxyTargets(props: {
name="method"
render={({ field }) => (
<FormItem>
<FormLabel>{t('method')}</FormLabel>
<FormLabel>
{t("method")}
</FormLabel>
<FormControl>
<Select
value={
@ -635,8 +606,15 @@ export default function ReverseProxyTargets(props: {
);
}}
>
<SelectTrigger id="method">
<SelectValue placeholder={t('methodSelect')} />
<SelectTrigger
id="method"
className="w-full"
>
<SelectValue
placeholder={t(
"methodSelect"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="http">
@ -662,7 +640,9 @@ export default function ReverseProxyTargets(props: {
name="ip"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>{t('targetAddr')}</FormLabel>
<FormLabel>
{t("targetAddr")}
</FormLabel>
<FormControl>
<Input id="ip" {...field} />
</FormControl>
@ -695,7 +675,9 @@ export default function ReverseProxyTargets(props: {
name="port"
render={({ field }) => (
<FormItem>
<FormLabel>{t('targetPort')}</FormLabel>
<FormLabel>
{t("targetPort")}
</FormLabel>
<FormControl>
<Input
id="port"
@ -710,11 +692,11 @@ export default function ReverseProxyTargets(props: {
/>
<Button
type="submit"
variant="outlinePrimary"
variant="secondary"
className="mt-6"
disabled={!(watchedIp && watchedPort)}
>
{t('targetSubmit')}
{t("targetSubmit")}
</Button>
</div>
</form>
@ -758,37 +740,26 @@ export default function ReverseProxyTargets(props: {
colSpan={columns.length}
className="h-24 text-center"
>
{t('targetNoOne')}
{t("targetNoOne")}
</TableCell>
</TableRow>
)}
</TableBody>
<TableCaption>
{t('targetNoOneDescription')}
</TableCaption>
{/* <TableCaption> */}
{/* {t('targetNoOneDescription')} */}
{/* </TableCaption> */}
</Table>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
onClick={saveTargets}
loading={targetsLoading}
disabled={targetsLoading}
form="targets-settings-form"
>
{t('targetsSubmit')}
</Button>
</SettingsSectionFooter>
</SettingsSection>
{resource.http && (
<SettingsSectionGrid cols={2}>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('targetTlsSettings')}
{t("proxyAdditional")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('targetTlsSettingsDescription')}
{t("proxyAdditionalDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@ -796,7 +767,7 @@ export default function ReverseProxyTargets(props: {
<Form {...tlsSettingsForm}>
<form
onSubmit={tlsSettingsForm.handleSubmit(
saveTlsSettings
saveAllSettings
)}
className="space-y-4"
id="tls-settings-form"
@ -809,16 +780,16 @@ export default function ReverseProxyTargets(props: {
<FormControl>
<SwitchInput
id="ssl-toggle"
label={t('proxyEnableSSL')}
label={t(
"proxyEnableSSL"
)}
defaultChecked={
field.value
}
onCheckedChange={(
val
) => {
field.onChange(
val
);
field.onChange(val);
}}
/>
</FormControl>
@ -838,7 +809,9 @@ export default function ReverseProxyTargets(props: {
className="p-0 flex items-center justify-start gap-2 w-full"
>
<p className="text-sm text-muted-foreground">
{t('targetTlsSettingsAdvanced')}
{t(
"targetTlsSettingsAdvanced"
)}
</p>
<div>
<ChevronsUpDown className="h-4 w-4" />
@ -858,15 +831,15 @@ export default function ReverseProxyTargets(props: {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('targetTlsSni')}
{t("targetTlsSni")}
</FormLabel>
<FormControl>
<Input
{...field}
/>
<Input {...field} />
</FormControl>
<FormDescription>
{t('targetTlsSniDescription')}
{t(
"targetTlsSniDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
@ -877,32 +850,12 @@ export default function ReverseProxyTargets(props: {
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={httpsTlsLoading}
form="tls-settings-form"
>
{t('targetTlsSubmit')}
</Button>
</SettingsSectionFooter>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('proxyAdditional')}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('proxyAdditionalDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...proxySettingsForm}>
<form
onSubmit={proxySettingsForm.handleSubmit(
saveProxySettings
saveAllSettings
)}
className="space-y-4"
id="proxy-settings-form"
@ -913,13 +866,15 @@ export default function ReverseProxyTargets(props: {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('proxyCustomHeader')}
{t("proxyCustomHeader")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t('proxyCustomHeaderDescription')}
{t(
"proxyCustomHeaderDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
@ -929,18 +884,26 @@ export default function ReverseProxyTargets(props: {
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={proxySettingsLoading}
form="proxy-settings-form"
>
{t('targetTlsSubmit')}
</Button>
</SettingsSectionFooter>
</SettingsSection>
</SettingsSectionGrid>
)}
<div className="flex justify-end mt-6">
<Button
onClick={saveAllSettings}
loading={
targetsLoading ||
httpsTlsLoading ||
proxySettingsLoading
}
disabled={
targetsLoading ||
httpsTlsLoading ||
proxySettingsLoading
}
>
{t("saveAllSettings")}
</Button>
</div>
</SettingsContainer>
);
}
@ -953,7 +916,7 @@ function isIPInSubnet(subnet: string, ip: string): boolean {
const t = useTranslations();
if (mask < 0 || mask > 32) {
throw new Error(t('subnetMaskErrorInvalid'));
throw new Error(t("subnetMaskErrorInvalid"));
}
// Convert IP addresses to binary numbers
@ -973,14 +936,14 @@ function ipToNumber(ip: string): number {
const t = useTranslations();
if (parts.length !== 4) {
throw new Error(t('ipAddressErrorInvalidFormat'));
throw new Error(t("ipAddressErrorInvalidFormat"));
}
// Convert IP octets to 32-bit number
return parts.reduce((num, octet) => {
const oct = parseInt(octet);
if (isNaN(oct) || oct < 0 || oct > 255) {
throw new Error(t('ipAddressErrorInvalidOctet'));
throw new Error(t("ipAddressErrorInvalidOctet"));
}
return (num << 8) + oct;
}, 0);

View file

@ -36,7 +36,6 @@ import {
TableBody,
TableCaption,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow
@ -543,48 +542,48 @@ export default function ResourceRules(props: {
return (
<SettingsContainer>
<Alert className="hidden md:block">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">{t('rulesAbout')}</AlertTitle>
<AlertDescription className="mt-4">
<div className="space-y-1 mb-4">
<p>
{t('rulesAboutDescription')}
</p>
</div>
<InfoSections cols={2}>
<InfoSection>
<InfoSectionTitle>{t('rulesActions')}</InfoSectionTitle>
<ul className="text-sm text-muted-foreground space-y-1">
<li className="flex items-center gap-2">
<Check className="text-green-500 w-4 h-4" />
{t('rulesActionAlwaysAllow')}
</li>
<li className="flex items-center gap-2">
<X className="text-red-500 w-4 h-4" />
{t('rulesActionAlwaysDeny')}
</li>
</ul>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t('rulesMatchCriteria')}
</InfoSectionTitle>
<ul className="text-sm text-muted-foreground space-y-1">
<li className="flex items-center gap-2">
{t('rulesMatchCriteriaIpAddress')}
</li>
<li className="flex items-center gap-2">
{t('rulesMatchCriteriaIpAddressRange')}
</li>
<li className="flex items-center gap-2">
{t('rulesMatchCriteriaUrl')}
</li>
</ul>
</InfoSection>
</InfoSections>
</AlertDescription>
</Alert>
{/* <Alert className="hidden md:block"> */}
{/* <InfoIcon className="h-4 w-4" /> */}
{/* <AlertTitle className="font-semibold">{t('rulesAbout')}</AlertTitle> */}
{/* <AlertDescription className="mt-4"> */}
{/* <div className="space-y-1 mb-4"> */}
{/* <p> */}
{/* {t('rulesAboutDescription')} */}
{/* </p> */}
{/* </div> */}
{/* <InfoSections cols={2}> */}
{/* <InfoSection> */}
{/* <InfoSectionTitle>{t('rulesActions')}</InfoSectionTitle> */}
{/* <ul className="text-sm text-muted-foreground space-y-1"> */}
{/* <li className="flex items-center gap-2"> */}
{/* <Check className="text-green-500 w-4 h-4" /> */}
{/* {t('rulesActionAlwaysAllow')} */}
{/* </li> */}
{/* <li className="flex items-center gap-2"> */}
{/* <X className="text-red-500 w-4 h-4" /> */}
{/* {t('rulesActionAlwaysDeny')} */}
{/* </li> */}
{/* </ul> */}
{/* </InfoSection> */}
{/* <InfoSection> */}
{/* <InfoSectionTitle> */}
{/* {t('rulesMatchCriteria')} */}
{/* </InfoSectionTitle> */}
{/* <ul className="text-sm text-muted-foreground space-y-1"> */}
{/* <li className="flex items-center gap-2"> */}
{/* {t('rulesMatchCriteriaIpAddress')} */}
{/* </li> */}
{/* <li className="flex items-center gap-2"> */}
{/* {t('rulesMatchCriteriaIpAddressRange')} */}
{/* </li> */}
{/* <li className="flex items-center gap-2"> */}
{/* {t('rulesMatchCriteriaUrl')} */}
{/* </li> */}
{/* </ul> */}
{/* </InfoSection> */}
{/* </InfoSections> */}
{/* </AlertDescription> */}
{/* </Alert> */}
<SettingsSection>
<SettingsSectionHeader>
@ -634,7 +633,7 @@ export default function ResourceRules(props: {
field.onChange
}
>
<SelectTrigger>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -664,7 +663,7 @@ export default function ResourceRules(props: {
field.onChange
}
>
<SelectTrigger>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -690,7 +689,7 @@ export default function ResourceRules(props: {
control={addRuleForm.control}
name="value"
render={({ field }) => (
<FormItem className="space-y-0 mb-2">
<FormItem className="gap-1">
<InfoPopup
text={t('value')}
info={
@ -710,8 +709,7 @@ export default function ResourceRules(props: {
/>
<Button
type="submit"
variant="outlinePrimary"
className="mb-2"
variant="secondary"
disabled={!rulesEnabled}
>
{t('ruleSubmit')}
@ -762,9 +760,9 @@ export default function ResourceRules(props: {
</TableRow>
)}
</TableBody>
<TableCaption>
{t('rulesOrder')}
</TableCaption>
{/* <TableCaption> */}
{/* {t('rulesOrder')} */}
{/* </TableCaption> */}
</Table>
</SettingsSectionBody>
<SettingsSectionFooter>

View file

@ -459,6 +459,7 @@ export default function Page() {
</SettingsSectionBody>
</SettingsSection>
{resourceTypes.length > 1 && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
@ -482,6 +483,7 @@ export default function Page() {
/>
</SettingsSectionBody>
</SettingsSection>
)}
{baseForm.watch("http") ? (
<SettingsSection>

View file

@ -392,7 +392,7 @@ export default function CreateShareLinkForm({
defaultValue={field.value.toString()}
>
<FormControl>
<SelectTrigger>
<SelectTrigger className="w-full">
<SelectValue placeholder={t('selectDuration')} />
</SelectTrigger>
</FormControl>

View file

@ -69,11 +69,8 @@ export default function ShareLinksTable({
async function deleteSharelink(id: string) {
await api.delete(`/access-token/${id}`).catch((e) => {
toast({
title: t('shareErrorDelete'),
description: formatAxiosError(
e,
t('shareErrorDeleteMessage')
)
title: t("shareErrorDelete"),
description: formatAxiosError(e, t("shareErrorDeleteMessage"))
});
});
@ -81,53 +78,12 @@ export default function ShareLinksTable({
setRows(newRows);
toast({
title: t('shareDeleted'),
description: t('shareDeletedDescription')
title: t("shareDeleted"),
description: t("shareDeletedDescription")
});
}
const columns: ColumnDef<ShareLinkRow>[] = [
{
id: "actions",
cell: ({ row }) => {
const router = useRouter();
const resourceRow = row.original;
return (
<>
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
{t('openMenu')}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
deleteSharelink(
resourceRow.accessTokenId
);
}}
>
<button className="text-red-500">
{t('delete')}
</button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
);
}
},
{
accessorKey: "resourceName",
header: ({ column }) => {
@ -138,7 +94,7 @@ export default function ShareLinksTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('resource')}
{t("resource")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -147,7 +103,7 @@ export default function ShareLinksTable({
const r = row.original;
return (
<Link href={`/${orgId}/settings/resources/${r.resourceId}`}>
<Button variant="outline">
<Button variant="outline" size="sm">
{r.resourceName}{" "}
{r.siteName ? `(${r.siteName})` : ""}
<ArrowUpRight className="ml-2 h-4 w-4" />
@ -166,7 +122,7 @@ export default function ShareLinksTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('title')}
{t("title")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -245,7 +201,7 @@ export default function ShareLinksTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('created')}
{t("created")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -265,7 +221,7 @@ export default function ShareLinksTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('expires')}
{t("expires")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -275,23 +231,50 @@ export default function ShareLinksTable({
if (r.expiresAt) {
return moment(r.expiresAt).format("lll");
}
return t('never');
return t("never");
}
},
{
id: "delete",
cell: ({ row }) => (
cell: ({ row }) => {
const resourceRow = row.original;
return (
<div className="flex items-center justify-end space-x-2">
{/* <DropdownMenu> */}
{/* <DropdownMenuTrigger asChild> */}
{/* <Button variant="ghost" className="h-8 w-8 p-0"> */}
{/* <span className="sr-only"> */}
{/* {t("openMenu")} */}
{/* </span> */}
{/* <MoreHorizontal className="h-4 w-4" /> */}
{/* </Button> */}
{/* </DropdownMenuTrigger> */}
{/* <DropdownMenuContent align="end"> */}
{/* <DropdownMenuItem */}
{/* onClick={() => { */}
{/* deleteSharelink( */}
{/* resourceRow.accessTokenId */}
{/* ); */}
{/* }} */}
{/* > */}
{/* <button className="text-red-500"> */}
{/* {t("delete")} */}
{/* </button> */}
{/* </DropdownMenuItem> */}
{/* </DropdownMenuContent> */}
{/* </DropdownMenu> */}
<Button
variant="outlinePrimary"
variant="secondary"
size="sm"
onClick={() =>
deleteSharelink(row.original.accessTokenId)
}
>
{t('delete')}
{t("delete")}
</Button>
</div>
)
);
}
}
];

View file

@ -28,7 +28,7 @@ import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import CreateSiteFormModal from "./CreateSiteModal";
import { useTranslations } from "next-intl";
import { parseDataSize } from '@app/lib/dataSize';
import { parseDataSize } from "@app/lib/dataSize";
export type SiteRow = {
id: number;
@ -81,11 +81,11 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
const deleteSite = (siteId: number) => {
api.delete(`/site/${siteId}`)
.catch((e) => {
console.error(t('siteErrorDelete'), e);
console.error(t("siteErrorDelete"), e);
toast({
variant: "destructive",
title: t('siteErrorDelete'),
description: formatAxiosError(e, t('siteErrorDelete'))
title: t("siteErrorDelete"),
description: formatAxiosError(e, t("siteErrorDelete"))
});
})
.then(() => {
@ -99,42 +99,6 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
};
const columns: ColumnDef<SiteRow>[] = [
{
id: "dots",
cell: ({ row }) => {
const siteRow = row.original;
const router = useRouter();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
>
<DropdownMenuItem>
{t('viewSettings')}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setSelectedSite(siteRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
},
{
accessorKey: "name",
header: ({ column }) => {
@ -145,7 +109,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -161,7 +125,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('online')}
{t("online")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -176,14 +140,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
return (
<span className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>{t('online')}</span>
<span>{t("online")}</span>
</span>
);
} else {
return (
<span className="text-neutral-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span>{t('offline')}</span>
<span>{t("offline")}</span>
</span>
);
}
@ -203,7 +167,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
}
className="hidden md:flex whitespace-nowrap"
>
{t('site')}
{t("site")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -226,13 +190,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('dataIn')}
{t("dataIn")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
sortingFn: (rowA, rowB) =>
parseDataSize(rowA.original.mbIn) - parseDataSize(rowB.original.mbIn)
parseDataSize(rowA.original.mbIn) -
parseDataSize(rowB.original.mbIn)
},
{
accessorKey: "mbOut",
@ -244,13 +209,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('dataOut')}
{t("dataOut")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
sortingFn: (rowA, rowB) =>
parseDataSize(rowA.original.mbOut) - parseDataSize(rowB.original.mbOut),
parseDataSize(rowA.original.mbOut) -
parseDataSize(rowB.original.mbOut)
},
{
accessorKey: "type",
@ -262,7 +228,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('connectionType')}
{t("connectionType")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -289,7 +255,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
if (originalRow.type === "local") {
return (
<div className="flex items-center space-x-2">
<span>{t('local')}</span>
<span>{t("local")}</span>
</div>
);
}
@ -316,12 +282,41 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
cell: ({ row }) => {
const siteRow = row.original;
return (
<div className="flex items-center justify-end">
<div className="flex items-center justify-end gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
>
<DropdownMenuItem>
{t("viewSettings")}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setSelectedSite(siteRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Link
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
>
<Button variant={"outlinePrimary"} className="ml-2">
{t('edit')}
<Button variant={"secondary"} size="sm">
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@ -343,22 +338,21 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
dialog={
<div className="space-y-4">
<p>
{t('siteQuestionRemove', {selectedSite: selectedSite?.name || selectedSite?.id})}
{t("siteQuestionRemove", {
selectedSite:
selectedSite?.name || selectedSite?.id
})}
</p>
<p>
{t('siteMessageRemove')}
</p>
<p>{t("siteMessageRemove")}</p>
<p>
{t('siteMessageConfirm')}
</p>
<p>{t("siteMessageConfirm")}</p>
</div>
}
buttonText={t('siteConfirmDelete')}
buttonText={t("siteConfirmDelete")}
onConfirm={async () => deleteSite(selectedSite!.id)}
string={selectedSite.name}
title={t('siteDelete')}
title={t("siteDelete")}
/>
)}

View file

@ -5,16 +5,7 @@ import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { authCookieHeader } from "@app/lib/api/cookies";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import SiteInfoCard from "./SiteInfoCard";
import { getTranslations } from "next-intl/server";

View file

@ -55,14 +55,6 @@ import {
import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios";
import { useParams, useRouter } from "next/navigation";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import Link from "next/link";
import { QRCodeCanvas } from "qrcode.react";
import { useTranslations } from "next-intl";
@ -117,7 +109,8 @@ export default function Page() {
.refine(
(data) => {
if (data.method !== "local") {
return data.copied;
// return data.copied;
return true;
}
return true;
},
@ -622,6 +615,7 @@ WantedBy=default.target`
</SettingsSectionBody>
</SettingsSection>
{tunnelTypes.length > 1 && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
@ -642,6 +636,7 @@ WantedBy=default.target`
/>
</SettingsSectionBody>
</SettingsSection>
)}
{form.watch("method") === "newt" && (
<>
@ -700,46 +695,46 @@ WantedBy=default.target`
</AlertDescription>
</Alert>
<Form {...form}>
<form
className="space-y-4"
id="create-site-form"
>
<FormField
control={form.control}
name="copied"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
defaultChecked={
form.getValues(
"copied"
) as boolean
}
onCheckedChange={(
e
) => {
form.setValue(
"copied",
e as boolean
);
}}
/>
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t('siteConfirmCopy')}
</label>
</div>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
{/* <Form {...form}> */}
{/* <form */}
{/* className="space-y-4" */}
{/* id="create-site-form" */}
{/* > */}
{/* <FormField */}
{/* control={form.control} */}
{/* name="copied" */}
{/* render={({ field }) => ( */}
{/* <FormItem> */}
{/* <div className="flex items-center space-x-2"> */}
{/* <Checkbox */}
{/* id="terms" */}
{/* defaultChecked={ */}
{/* form.getValues( */}
{/* "copied" */}
{/* ) as boolean */}
{/* } */}
{/* onCheckedChange={( */}
{/* e */}
{/* ) => { */}
{/* form.setValue( */}
{/* "copied", */}
{/* e as boolean */}
{/* ); */}
{/* }} */}
{/* /> */}
{/* <label */}
{/* htmlFor="terms" */}
{/* className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" */}
{/* > */}
{/* {t('siteConfirmCopy')} */}
{/* </label> */}
{/* </div> */}
{/* <FormMessage /> */}
{/* </FormItem> */}
{/* )} */}
{/* /> */}
{/* </form> */}
{/* </Form> */}
</SettingsSectionBody>
</SettingsSection>
<SettingsSection>

View file

@ -46,11 +46,14 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
const deleteSite = (apiKeyId: string) => {
api.delete(`/api-key/${apiKeyId}`)
.catch((e) => {
console.error(t('apiKeysErrorDelete'), e);
console.error(t("apiKeysErrorDelete"), e);
toast({
variant: "destructive",
title: t('apiKeysErrorDelete'),
description: formatAxiosError(e, t('apiKeysErrorDeleteMessage'))
title: t("apiKeysErrorDelete"),
description: formatAxiosError(
e,
t("apiKeysErrorDeleteMessage")
)
});
})
.then(() => {
@ -64,41 +67,6 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
};
const columns: ColumnDef<ApiKeyRow>[] = [
{
id: "dots",
cell: ({ row }) => {
const apiKeyROw = row.original;
const router = useRouter();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">{t('openMenu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setSelected(apiKeyROw);
}}
>
<span>{t('viewSettings')}</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSelected(apiKeyROw);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
},
{
accessorKey: "name",
header: ({ column }) => {
@ -109,7 +77,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -117,7 +85,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
},
{
accessorKey: "key",
header: t('key'),
header: t("key"),
cell: ({ row }) => {
const r = row.original;
return <span className="font-mono">{r.key}</span>;
@ -125,7 +93,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
},
{
accessorKey: "createdAt",
header: t('createdAt'),
header: t("createdAt"),
cell: ({ row }) => {
const r = row.original;
return <span>{moment(r.createdAt).format("lll")} </span>;
@ -136,14 +104,45 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
cell: ({ row }) => {
const r = row.original;
return (
<div className="flex items-center justify-end gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setSelected(r);
}}
>
<span>{t("viewSettings")}</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSelected(r);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center justify-end">
<Link href={`/admin/api-keys/${r.id}`}>
<Button variant={"outlinePrimary"} className="ml-2">
{t('edit')}
<Button variant={"secondary"} className="ml-2" size="sm">
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
</div>
</div>
);
}
}
@ -161,24 +160,23 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
dialog={
<div className="space-y-4">
<p>
{t('apiKeysQuestionRemove', {selectedApiKey: selected?.name || selected?.id})}
{t("apiKeysQuestionRemove", {
selectedApiKey:
selected?.name || selected?.id
})}
</p>
<p>
<b>
{t('apiKeysMessageRemove')}
</b>
<b>{t("apiKeysMessageRemove")}</b>
</p>
<p>
{t('apiKeysMessageConfirm')}
</p>
<p>{t("apiKeysMessageConfirm")}</p>
</div>
}
buttonText={t('apiKeysDeleteConfirm')}
buttonText={t("apiKeysDeleteConfirm")}
onConfirm={async () => deleteSite(selected!.id)}
string={selected.name}
title={t('apiKeysDelete')}
title={t("apiKeysDelete")}
/>
)}

View file

@ -2,16 +2,7 @@ import { internal } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { authCookieHeader } from "@app/lib/api/cookies";
import { SidebarSettings } from "@app/components/SidebarSettings";
import Link from "next/link";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import { GetApiKeyResponse } from "@server/routers/apiKeys";
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
import { HorizontalTabs } from "@app/components/HorizontalTabs";

View file

@ -25,21 +25,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@app/components/ui/input";
import { InfoIcon } from "lucide-react";
import { Button } from "@app/components/ui/button";
import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios";
import { useParams, useRouter } from "next/navigation";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
CreateOrgApiKeyBody,
CreateOrgApiKeyResponse
@ -108,7 +99,7 @@ export default function Page() {
const copiedForm = useForm<CopiedFormValues>({
resolver: zodResolver(copiedFormSchema),
defaultValues: {
copied: false
copied: true
}
});
@ -299,54 +290,54 @@ export default function Page() {
</AlertDescription>
</Alert>
<h4 className="font-semibold">
{t('apiKeysInfo')}
</h4>
{/* <h4 className="font-semibold"> */}
{/* {t('apiKeysInfo')} */}
{/* </h4> */}
<CopyTextBox
text={`${apiKey.apiKeyId}.${apiKey.apiKey}`}
/>
<Form {...copiedForm}>
<form
className="space-y-4"
id="copied-form"
>
<FormField
control={copiedForm.control}
name="copied"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
defaultChecked={
copiedForm.getValues(
"copied"
) as boolean
}
onCheckedChange={(
e
) => {
copiedForm.setValue(
"copied",
e as boolean
);
}}
/>
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t('apiKeysConfirmCopy')}
</label>
</div>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
{/* <Form {...copiedForm}> */}
{/* <form */}
{/* className="space-y-4" */}
{/* id="copied-form" */}
{/* > */}
{/* <FormField */}
{/* control={copiedForm.control} */}
{/* name="copied" */}
{/* render={({ field }) => ( */}
{/* <FormItem> */}
{/* <div className="flex items-center space-x-2"> */}
{/* <Checkbox */}
{/* id="terms" */}
{/* defaultChecked={ */}
{/* copiedForm.getValues( */}
{/* "copied" */}
{/* ) as boolean */}
{/* } */}
{/* onCheckedChange={( */}
{/* e */}
{/* ) => { */}
{/* copiedForm.setValue( */}
{/* "copied", */}
{/* e as boolean */}
{/* ); */}
{/* }} */}
{/* /> */}
{/* <label */}
{/* htmlFor="terms" */}
{/* className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" */}
{/* > */}
{/* {t('apiKeysConfirmCopy')} */}
{/* </label> */}
{/* </div> */}
{/* <FormMessage /> */}
{/* </FormItem> */}
{/* )} */}
{/* /> */}
{/* </form> */}
{/* </Form> */}
</SettingsSectionBody>
</SettingsSection>
)}

View file

@ -43,14 +43,14 @@ export default function IdpTable({ idps }: Props) {
try {
await api.delete(`/idp/${idpId}`);
toast({
title: t('success'),
description: t('idpDeletedDescription')
title: t("success"),
description: t("idpDeletedDescription")
});
setIsDeleteModalOpen(false);
router.refresh();
} catch (e) {
toast({
title: t('error'),
title: t("error"),
description: formatAxiosError(e),
variant: "destructive"
});
@ -67,41 +67,6 @@ export default function IdpTable({ idps }: Props) {
};
const columns: ColumnDef<IdpRow>[] = [
{
id: "dots",
cell: ({ row }) => {
const r = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">{t('openMenu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/admin/idp/${r.idpId}/general`}
>
<DropdownMenuItem>
{t('viewSettings')}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setSelectedIdp(r);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
},
{
accessorKey: "idpId",
header: ({ column }) => {
@ -128,7 +93,7 @@ export default function IdpTable({ idps }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -144,7 +109,7 @@ export default function IdpTable({ idps }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('type')}
{t("type")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -162,9 +127,43 @@ export default function IdpTable({ idps }: Props) {
const siteRow = row.original;
return (
<div className="flex items-center justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/admin/idp/${siteRow.idpId}/general`}
>
<DropdownMenuItem>
{t("viewSettings")}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setSelectedIdp(siteRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Link href={`/admin/idp/${siteRow.idpId}/general`}>
<Button variant={"outlinePrimary"} className="ml-2">
{t('edit')}
<Button
variant={"secondary"}
className="ml-2"
size="sm"
>
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@ -186,22 +185,20 @@ export default function IdpTable({ idps }: Props) {
dialog={
<div className="space-y-4">
<p>
{t('idpQuestionRemove', {name: selectedIdp.name})}
{t("idpQuestionRemove", {
name: selectedIdp.name
})}
</p>
<p>
<b>
{t('idpMessageRemove')}
</b>
</p>
<p>
{t('idpMessageConfirm')}
<b>{t("idpMessageRemove")}</b>
</p>
<p>{t("idpMessageConfirm")}</p>
</div>
}
buttonText={t('idpConfirmDelete')}
buttonText={t("idpConfirmDelete")}
onConfirm={async () => deleteIdp(selectedIdp.idpId)}
string={selectedIdp.name}
title={t('idpDelete')}
title={t("idpDelete")}
/>
)}

View file

@ -4,17 +4,7 @@ import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { authCookieHeader } from "@app/lib/api/cookies";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import { ProfessionalContentOverlay } from "@app/components/ProfessionalContentOverlay";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import { getTranslations } from "next-intl/server";
interface SettingsLayoutProps {

View file

@ -140,7 +140,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
return (
<div className="flex items-center justify-end">
<Button
variant={"outlinePrimary"}
variant={"secondary"}
className="ml-2"
onClick={() => onEdit(policy)}
>

View file

@ -9,7 +9,7 @@ import { internal } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { authCookieHeader } from "@app/lib/api/cookies";
import { Layout } from "@app/components/Layout";
import { adminNavItems } from "../navigation";
import { adminNavSections } from "../navigation";
export const dynamic = "force-dynamic";
@ -47,7 +47,7 @@ export default async function AdminLayout(props: LayoutProps) {
return (
<UserProvider user={user}>
<Layout orgs={orgs} navItems={adminNavItems}>
<Layout orgs={orgs} navItems={adminNavSections}>
{props.children}
</Layout>
</UserProvider>

View file

@ -122,7 +122,7 @@ export function LicenseKeysDataTable({
cell: ({ row }) => (
<div className="flex items-center justify-end space-x-2">
<Button
variant="outlinePrimary"
variant="secondary"
onClick={() => onDelete(row.original)}
>
{t('delete')}

View file

@ -41,11 +41,11 @@ export default function UsersTable({ users }: Props) {
const deleteUser = (id: string) => {
api.delete(`/user/${id}`)
.catch((e) => {
console.error(t('userErrorDelete'), e);
console.error(t("userErrorDelete"), e);
toast({
variant: "destructive",
title: t('userErrorDelete'),
description: formatAxiosError(e, t('userErrorDelete'))
title: t("userErrorDelete"),
description: formatAxiosError(e, t("userErrorDelete"))
});
})
.then(() => {
@ -84,7 +84,7 @@ export default function UsersTable({ users }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('username')}
{t("username")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -100,7 +100,7 @@ export default function UsersTable({ users }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('email')}
{t("email")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -116,7 +116,7 @@ export default function UsersTable({ users }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -132,7 +132,7 @@ export default function UsersTable({ users }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('identityProvider')}
{t("identityProvider")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -146,14 +146,15 @@ export default function UsersTable({ users }: Props) {
<>
<div className="flex items-center justify-end">
<Button
variant={"outlinePrimary"}
variant={"secondary"}
size="sm"
className="ml-2"
onClick={() => {
setSelected(r);
setIsDeleteModalOpen(true);
}}
>
{t('delete')}
{t("delete")}
</Button>
</div>
</>
@ -174,26 +175,27 @@ export default function UsersTable({ users }: Props) {
dialog={
<div className="space-y-4">
<p>
{t('userQuestionRemove', {selectedUser: selected?.email || selected?.name || selected?.username})}
{t("userQuestionRemove", {
selectedUser:
selected?.email ||
selected?.name ||
selected?.username
})}
</p>
<p>
<b>
{t('userMessageRemove')}
</b>
<b>{t("userMessageRemove")}</b>
</p>
<p>
{t('userMessageConfirm')}
</p>
<p>{t("userMessageConfirm")}</p>
</div>
}
buttonText={t('userDeleteConfirm')}
buttonText={t("userDeleteConfirm")}
onConfirm={async () => deleteUser(selected!.id)}
string={
selected.email || selected.name || selected.username
}
title={t('userDeleteServer')}
title={t("userDeleteServer")}
/>
)}

View file

@ -1,62 +1,76 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Space+Grotesk:wght@300..700&display=swap");
@import 'tw-animate-css';
@import 'tailwindcss';
@import "tw-animate-css";
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
:root {
--background: hsl(0 0% 98%);
--foreground: hsl(20 0% 10%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(20 0% 10%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(20 0% 10%);
--primary: hsl(24.6 95% 53.1%);
--primary-foreground: hsl(60 9.1% 97.8%);
--secondary: hsl(60 4.8% 95.9%);
--secondary-foreground: hsl(24 9.8% 10%);
--muted: hsl(60 4.8% 85%);
--muted-foreground: hsl(25 5.3% 44.7%);
--accent: hsl(60 4.8% 90%);
--accent-foreground: hsl(24 9.8% 10%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(60 9.1% 97.8%);
--border: hsl(20 5.9% 90%);
--input: hsl(20 5.9% 75%);
--ring: hsl(20 5.9% 75%);
--radius: 0.75rem;
--chart-1: hsl(12 76% 61%);
--chart-2: hsl(173 58% 39%);
--chart-3: hsl(197 37% 24%);
--chart-4: hsl(43 74% 66%);
--chart-5: hsl(27 87% 67%);
--radius: 0.65rem;
--background: oklch(0.99 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.6717 0.1946 41.93);
--primary-foreground: oklch(0.98 0.016 73.684);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.213 47.604);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.705 0.213 47.604);
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.213 47.604);
}
.dark {
--background: hsl(20 0% 8%);
--foreground: hsl(60 9.1% 97.8%);
--card: hsl(20 0% 10%);
--card-foreground: hsl(60 9.1% 97.8%);
--popover: hsl(20 0% 10%);
--popover-foreground: hsl(60 9.1% 97.8%);
--primary: hsl(20.5 90.2% 48.2%);
--primary-foreground: hsl(60 9.1% 97.8%);
--secondary: hsl(12 6.5% 15%);
--secondary-foreground: hsl(60 9.1% 97.8%);
--muted: hsl(12 6.5% 25%);
--muted-foreground: hsl(24 5.4% 63.9%);
--accent: hsl(12 2.5% 15%);
--accent-foreground: hsl(60 9.1% 97.8%);
--destructive: hsl(0 72.2% 50.6%);
--destructive-foreground: hsl(60 9.1% 97.8%);
--border: hsl(12 6.5% 15%);
--input: hsl(12 6.5% 35%);
--ring: hsl(12 6.5% 35%);
--chart-1: hsl(220 70% 50%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
--background: oklch(0.20 0.006 285.885);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.6717 0.1946 41.93);
--primary-foreground: oklch(0.98 0.016 73.684);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.646 0.222 41.116);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.646 0.222 41.116);
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.646 0.222 41.116);
}
@theme inline {

View file

@ -11,7 +11,7 @@ import { AxiosResponse } from "axios";
import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey";
import LicenseStatusProvider from "@app/providers/LicenseStatusProvider";
import { GetLicenseStatusResponse } from "@server/routers/license";
import LicenseViolation from "./components/LicenseViolation";
import LicenseViolation from "@app/components/LicenseViolation";
import { cache } from "react";
import { NextIntlClientProvider } from "next-intl";
import { getLocale } from "next-intl/server";

View file

@ -9,26 +9,19 @@ import {
Fingerprint,
Workflow,
KeyRound,
TicketCheck
TicketCheck,
User
} from "lucide-react";
export const orgLangingNavItems: SidebarNavItem[] = [
{
title: "sidebarOverview",
href: "/{orgId}",
icon: <Home className="h-4 w-4" />
}
];
export type SidebarNavSection = {
heading: string;
items: SidebarNavItem[];
};
export const rootNavItems: SidebarNavItem[] = [
export const orgNavSections: SidebarNavSection[] = [
{
title: "sidebarHome",
href: "/",
icon: <Home className="h-4 w-4" />
}
];
export const orgNavItems: SidebarNavItem[] = [
heading: "General",
items: [
{
title: "sidebarSites",
href: "/{orgId}/settings/sites",
@ -38,34 +31,37 @@ export const orgNavItems: SidebarNavItem[] = [
title: "sidebarResources",
href: "/{orgId}/settings/resources",
icon: <Waypoints className="h-4 w-4" />
}
]
},
{
title: "sidebarAccessControl",
href: "/{orgId}/settings/access",
icon: <Users className="h-4 w-4" />,
autoExpand: true,
children: [
heading: "Access Control",
items: [
{
title: "sidebarUsers",
href: "/{orgId}/settings/access/users",
children: [
{
title: "sidebarInvitations",
href: "/{orgId}/settings/access/invitations"
}
]
icon: <User className="h-4 w-4" />
},
{
title: "sidebarRoles",
href: "/{orgId}/settings/access/roles"
}
]
href: "/{orgId}/settings/access/roles",
icon: <Users className="h-4 w-4" />
},
{
title: "sidebarInvitations",
href: "/{orgId}/settings/access/invitations",
icon: <TicketCheck className="h-4 w-4" />
},
{
title: "sidebarShareableLinks",
href: "/{orgId}/settings/share-links",
icon: <LinkIcon className="h-4 w-4" />
}
]
},
{
heading: "Organization",
items: [
{
title: "sidebarApiKeys",
href: "/{orgId}/settings/api-keys",
@ -76,9 +72,14 @@ export const orgNavItems: SidebarNavItem[] = [
href: "/{orgId}/settings/general",
icon: <Settings className="h-4 w-4" />
}
]
}
];
export const adminNavItems: SidebarNavItem[] = [
export const adminNavSections: SidebarNavSection[] = [
{
heading: "Admin",
items: [
{
title: "sidebarAllUsers",
href: "/admin/users",
@ -99,4 +100,6 @@ export const adminNavItems: SidebarNavItem[] = [
href: "/admin/license",
icon: <TicketCheck className="h-4 w-4" />
}
]
}
];

View file

@ -6,11 +6,10 @@ import { ListUserOrgsResponse } from "@server/routers/org";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { cache } from "react";
import OrganizationLanding from "./components/OrganizationLanding";
import OrganizationLanding from "@app/components/OrganizationLanding";
import { pullEnv } from "@app/lib/pullEnv";
import { cleanRedirect } from "@app/lib/cleanRedirect";
import { Layout } from "@app/components/Layout";
import { rootNavItems } from "./navigation";
import { InitialSetupCompleteResponse } from "@server/routers/auth";
import { cookies } from "next/headers";
@ -73,17 +72,15 @@ export default async function Page(props: {
}
}
// Check for pangolin-last-org cookie and redirect if valid
const allCookies = await cookies();
const lastOrgCookie = allCookies.get("pangolin-last-org")?.value;
if (lastOrgCookie && orgs.length > 0) {
// Check if the last org from cookie exists in user's organizations
const lastOrgExists = orgs.some(org => org.orgId === lastOrgCookie);
const lastOrgExists = orgs.some((org) => org.orgId === lastOrgCookie);
if (lastOrgExists) {
redirect(`/${lastOrgCookie}`);
} else {
const ownedOrg = orgs.find(org => org.isOwner);
const ownedOrg = orgs.find((org) => org.isOwner);
if (ownedOrg) {
redirect(`/${ownedOrg.orgId}`);
} else {
@ -94,7 +91,7 @@ export default async function Page(props: {
return (
<UserProvider user={user}>
<Layout orgs={orgs} navItems={rootNavItems} showBreadcrumbs={false}>
<Layout orgs={orgs} navItems={[]}>
<div className="w-full max-w-md mx-auto md:mt-32 mt-4">
<OrganizationLanding
disableCreateOrg={

View file

@ -6,7 +6,6 @@ import UserProvider from "@app/providers/UserProvider";
import { Metadata } from "next";
import { redirect } from "next/navigation";
import { cache } from "react";
import { rootNavItems } from "../navigation";
import { ListUserOrgsResponse } from "@server/routers/org";
import { internal } from "@app/lib/api";
import { AxiosResponse } from "axios";
@ -54,11 +53,7 @@ export default async function SetupLayout({
return (
<>
<UserProvider user={user}>
<Layout
navItems={rootNavItems}
showBreadcrumbs={false}
orgs={orgs}
>
<Layout navItems={[]} orgs={orgs}>
<div className="w-full max-w-2xl mx-auto md:mt-32 mt-4">
{children}
</div>

View file

@ -1,3 +0,0 @@
"use client";
export function AuthFooter() {}

View file

@ -1,42 +0,0 @@
"use client";
import { usePathname } from "next/navigation";
import Link from "next/link";
import { ChevronRight } from "lucide-react";
import { cn } from "@app/lib/cn";
interface BreadcrumbItem {
label: string;
href: string;
}
export function Breadcrumbs() {
const pathname = usePathname();
const segments = pathname.split("/").filter(Boolean);
const breadcrumbs: BreadcrumbItem[] = segments.map((segment, index) => {
const href = `/${segments.slice(0, index + 1).join("/")}`;
let label = decodeURIComponent(segment);
return { label, href };
});
return (
<nav className="flex items-center space-x-1 text-muted-foreground">
{breadcrumbs.map((crumb, index) => (
<div key={crumb.href} className="flex items-center flex-nowrap">
{index !== 0 && <ChevronRight className="h-4 w-4 flex-shrink-0" />}
<Link
href={crumb.href}
className={cn(
"ml-1 hover:text-foreground whitespace-nowrap",
index === breadcrumbs.length - 1 &&
"text-foreground font-medium"
)}
>
{crumb.label}
</Link>
</div>
))}
</nav>
);
}

View file

@ -72,7 +72,7 @@ export default function InviteUserForm({
const formSchema = z.object({
string: z.string().refine((val) => val === string, {
message: t('inviteErrorInvalidConfirmation')
message: t("inviteErrorInvalidConfirmation")
})
});
@ -108,7 +108,9 @@ export default function InviteUserForm({
<CredenzaTitle>{title}</CredenzaTitle>
</CredenzaHeader>
<CredenzaBody>
<div className="mb-4 break-all overflow-hidden">{dialog}</div>
<div className="mb-4 break-all overflow-hidden">
{dialog}
</div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
@ -132,9 +134,10 @@ export default function InviteUserForm({
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button
variant={"destructive"}
type="submit"
form="confirm-delete-form"
loading={loading}

View file

@ -40,7 +40,7 @@ export default function CopyTextBox({
>
<pre
ref={textRef}
className={`p-2 pr-16 text-sm w-full ${
className={`p-4 pr-16 text-sm w-full ${
wrapText
? "whitespace-pre-wrap break-words"
: "overflow-x-auto"

View file

@ -32,7 +32,7 @@ const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) =>
href={text}
target="_blank"
rel="noopener noreferrer"
className="truncate hover:underline"
className="truncate hover:underline text-sm"
style={{ maxWidth: "100%" }} // Ensures truncation works within parent
title={text} // Shows full text on hover
>
@ -40,7 +40,7 @@ const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) =>
</Link>
) : (
<span
className="truncate"
className="truncate text-sm"
style={{
maxWidth: "100%",
display: "block",

View file

@ -1,270 +1,73 @@
"use client";
import React, { useEffect, useState } from "react";
import { SidebarNav } from "@app/components/SidebarNav";
import { OrgSelector } from "@app/components/OrgSelector";
import React from "react";
import { cn } from "@app/lib/cn";
import { ListUserOrgsResponse } from "@server/routers/org";
import SupporterStatus from "@app/components/SupporterStatus";
import { Button } from "@app/components/ui/button";
import { ExternalLink, Menu, X, Server } from "lucide-react";
import Image from "next/image";
import ProfileIcon from "@app/components/ProfileIcon";
import {
Sheet,
SheetContent,
SheetTrigger,
SheetTitle,
SheetDescription
} from "@app/components/ui/sheet";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { Breadcrumbs } from "@app/components/Breadcrumbs";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useUserContext } from "@app/hooks/useUserContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useTheme } from "next-themes";
import { useTranslations } from "next-intl";
import type { SidebarNavSection } from "@app/app/navigation";
import { LayoutSidebar } from "@app/components/LayoutSidebar";
import { LayoutHeader } from "@app/components/LayoutHeader";
import { LayoutMobileMenu } from "@app/components/LayoutMobileMenu";
import { cookies } from "next/headers";
interface LayoutProps {
children: React.ReactNode;
orgId?: string;
orgs?: ListUserOrgsResponse["orgs"];
navItems?: Array<{
title: string;
href: string;
icon?: React.ReactNode;
children?: Array<{
title: string;
href: string;
icon?: React.ReactNode;
}>;
}>;
navItems?: SidebarNavSection[];
showSidebar?: boolean;
showBreadcrumbs?: boolean;
showHeader?: boolean;
showTopBar?: boolean;
defaultSidebarCollapsed?: boolean;
}
export function Layout({
export async function Layout({
children,
orgId,
orgs,
navItems = [],
showSidebar = true,
showBreadcrumbs = true,
showHeader = true,
showTopBar = true
showTopBar = true,
defaultSidebarCollapsed = false
}: LayoutProps) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const { env } = useEnvContext();
const pathname = usePathname();
const isAdminPage = pathname?.startsWith("/admin");
const { user } = useUserContext();
const { isUnlocked } = useLicenseStatusContext();
const allCookies = await cookies();
const sidebarStateCookie = allCookies.get("pangolin-sidebar-state")?.value;
const { theme } = useTheme();
const [path, setPath] = useState<string>(""); // Default logo path
useEffect(() => {
function getPath() {
let lightOrDark = theme;
if (theme === "system" || !theme) {
lightOrDark = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
}
if (lightOrDark === "light") {
// return "/logo/word_mark_black.png";
return "/logo/pangolin_orange.svg";
}
// return "/logo/word_mark_white.png";
return "/logo/pangolin_orange.svg";
}
setPath(getPath());
}, [theme, env]);
const t = useTranslations();
const initialSidebarCollapsed = sidebarStateCookie === "collapsed" ||
(sidebarStateCookie !== "expanded" && defaultSidebarCollapsed);
return (
<div className="flex flex-col h-screen overflow-hidden">
{/* Full width header */}
{showHeader && (
<div className="border-b shrink-0 bg-card">
<div className="h-16 flex items-center px-4">
<div className="flex items-center gap-4">
{showSidebar && (
<div className="md:hidden">
<Sheet
open={isMobileMenuOpen}
onOpenChange={setIsMobileMenuOpen}
>
<SheetTrigger asChild>
<Button variant="ghost" size="icon">
<Menu className="h-6 w-6" />
</Button>
</SheetTrigger>
<SheetContent
side="left"
className="w-64 p-0 flex flex-col h-full"
>
<SheetTitle className="sr-only">
{t('navbar')}
</SheetTitle>
<SheetDescription className="sr-only">
{t('navbarDescription')}
</SheetDescription>
<div className="flex-1 overflow-y-auto">
<div className="p-4">
<SidebarNav
items={navItems}
onItemClick={() =>
setIsMobileMenuOpen(
false
)
}
/>
</div>
{!isAdminPage &&
user.serverAdmin && (
<div className="p-4 border-t">
<Link
href="/admin"
className="flex items-center gap-3 text-muted-foreground hover:text-foreground transition-colors px-3 py-2 rounded-md w-full"
onClick={() =>
setIsMobileMenuOpen(
false
)
}
>
<Server className="h-4 w-4" />
{t('serverAdmin')}
</Link>
</div>
)}
</div>
<div className="p-4 space-y-4 border-t shrink-0">
<SupporterStatus />
<OrgSelector
orgId={orgId}
orgs={orgs}
/>
{env?.app?.version && (
<div className="text-xs text-muted-foreground text-center">
v{env.app.version}
</div>
)}
</div>
</SheetContent>
</Sheet>
</div>
)}
<Link
href="/"
className="flex items-center hidden md:block"
>
{path && (
<Image
src={path}
alt="Pangolin Logo"
width={35}
height={35}
priority={true}
quality={25}
/>
)}
</Link>
{showBreadcrumbs && (
<div className="hidden md:block overflow-x-auto scrollbar-hide">
<Breadcrumbs />
</div>
)}
</div>
{showTopBar && (
<div className="ml-auto flex items-center justify-end md:justify-between">
<div className="hidden md:flex items-center space-x-3 mr-6">
<Link
href="https://docs.fossorial.io"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
>
{t('navbarDocsLink')}
</Link>
</div>
<div>
<ProfileIcon />
</div>
</div>
)}
</div>
{showBreadcrumbs && (
<div className="md:hidden px-4 pb-2 overflow-x-auto scrollbar-hide">
<Breadcrumbs />
</div>
)}
</div>
)}
<div className="flex flex-1 overflow-hidden">
<div className="flex h-screen overflow-hidden">
{/* Desktop Sidebar */}
{showSidebar && (
<div className="hidden md:flex w-64 border-r bg-card flex-col h-full shrink-0">
<div className="flex-1 overflow-y-auto">
<div className="p-4">
<SidebarNav items={navItems} />
</div>
{!isAdminPage && user.serverAdmin && (
<div className="p-4 border-t">
<Link
href="/admin"
className="flex items-center gap-3 text-muted-foreground hover:text-foreground transition-colors px-3 py-2 rounded-md w-full"
>
<Server className="h-4 w-4" />
{t('serverAdmin')}
</Link>
</div>
)}
</div>
<div className="p-4 space-y-4 border-t shrink-0">
<SupporterStatus />
<OrgSelector orgId={orgId} orgs={orgs} />
<div className="space-y-2">
<div className="text-xs text-muted-foreground text-center">
<Link
href="https://github.com/fosrl/pangolin"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-1"
>
{!isUnlocked()
? t('communityEdition')
: t('commercialEdition')}
<ExternalLink size={12} />
</Link>
</div>
{env?.app?.version && (
<div className="text-xs text-muted-foreground text-center">
v{env.app.version}
</div>
)}
</div>
</div>
</div>
<LayoutSidebar
orgId={orgId}
orgs={orgs}
navItems={navItems}
defaultSidebarCollapsed={initialSidebarCollapsed}
/>
)}
{/* Main content */}
{/* Main content area */}
<div
className={cn(
"flex-1 flex flex-col h-full min-w-0",
!showSidebar && "w-full"
)}
>
{/* Mobile header */}
{showHeader && (
<LayoutMobileMenu
orgId={orgId}
orgs={orgs}
navItems={navItems}
showSidebar={showSidebar}
showTopBar={showTopBar}
/>
)}
{/* Desktop header */}
{showHeader && <LayoutHeader showTopBar={showTopBar} />}
{/* Main content */}
<main className="flex-1 overflow-y-auto p-3 md:p-6 w-full">
<div className="container mx-auto max-w-12xl mb-12">
{children}
@ -272,6 +75,5 @@ export function Layout({
</main>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,73 @@
"use client";
import React, { useEffect, useState } from "react";
import { cn } from "@app/lib/cn";
import Image from "next/image";
import Link from "next/link";
import ProfileIcon from "@app/components/ProfileIcon";
import ThemeSwitcher from "@app/components/ThemeSwitcher";
import { useTheme } from "next-themes";
interface LayoutHeaderProps {
showTopBar: boolean;
}
export function LayoutHeader({ showTopBar }: LayoutHeaderProps) {
const { theme } = useTheme();
const [path, setPath] = useState<string>("");
useEffect(() => {
function getPath() {
let lightOrDark = theme;
if (theme === "system" || !theme) {
lightOrDark = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
}
if (lightOrDark === "light") {
return "/logo/word_mark_black.png";
}
return "/logo/word_mark_white.png";
}
setPath(getPath());
}, [theme]);
return (
<div className="shrink-0 hidden md:block">
<div className="px-6 py-2">
<div className="container mx-auto max-w-12xl">
<div className="h-16 flex items-center justify-between">
<div className="flex items-center">
<Link href="/" className="flex items-center">
{path && (
<Image
src={path}
alt="Pangolin"
width={98}
height={32}
className="h-8 w-auto"
/>
)}
</Link>
</div>
{/* Profile controls on the right */}
{showTopBar && (
<div className="flex items-center space-x-2">
<ThemeSwitcher />
<ProfileIcon />
</div>
)}
</div>
</div>
</div>
</div>
);
}
export default LayoutHeader;

View file

@ -0,0 +1,142 @@
"use client";
import React, { useState } from "react";
import { SidebarNav } from "@app/components/SidebarNav";
import { OrgSelector } from "@app/components/OrgSelector";
import { cn } from "@app/lib/cn";
import { ListUserOrgsResponse } from "@server/routers/org";
import SupporterStatus from "@app/components/SupporterStatus";
import { Button } from "@app/components/ui/button";
import { Menu, Server } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useUserContext } from "@app/hooks/useUserContext";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
import ProfileIcon from "@app/components/ProfileIcon";
import ThemeSwitcher from "@app/components/ThemeSwitcher";
import type { SidebarNavSection } from "@app/app/navigation";
import {
Sheet,
SheetContent,
SheetTrigger,
SheetTitle,
SheetDescription
} from "@app/components/ui/sheet";
import { Abel } from "next/font/google";
interface LayoutMobileMenuProps {
orgId?: string;
orgs?: ListUserOrgsResponse["orgs"];
navItems: SidebarNavSection[];
showSidebar: boolean;
showTopBar: boolean;
}
export function LayoutMobileMenu({
orgId,
orgs,
navItems,
showSidebar,
showTopBar
}: LayoutMobileMenuProps) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const pathname = usePathname();
const isAdminPage = pathname?.startsWith("/admin");
const { user } = useUserContext();
const { env } = useEnvContext();
const t = useTranslations();
return (
<div className="shrink-0 md:hidden">
<div className="h-16 flex items-center px-4">
<div className="flex items-center gap-4">
{showSidebar && (
<div>
<Sheet
open={isMobileMenuOpen}
onOpenChange={setIsMobileMenuOpen}
>
<SheetTrigger asChild>
<Button variant="ghost" size="icon">
<Menu className="h-6 w-6" />
</Button>
</SheetTrigger>
<SheetContent
side="left"
className="w-64 p-0 flex flex-col h-full"
>
<SheetTitle className="sr-only">
{t("navbar")}
</SheetTitle>
<SheetDescription className="sr-only">
{t("navbarDescription")}
</SheetDescription>
<div className="flex-1 overflow-y-auto">
<div className="p-4">
<OrgSelector
orgId={orgId}
orgs={orgs}
/>
</div>
<div className="px-4">
{!isAdminPage &&
user.serverAdmin && (
<div className="pb-3">
<Link
href="/admin"
className={cn(
"flex items-center rounded transition-colors text-muted-foreground hover:text-foreground text-sm w-full hover:bg-secondary/50 dark:hover:bg-secondary/20 rounded-md px-3 py-1.5"
)}
onClick={() =>
setIsMobileMenuOpen(
false
)
}
>
<span className="flex-shrink-0 mr-2">
<Server className="h-4 w-4" />
</span>
<span>
{t(
"serverAdmin"
)}
</span>
</Link>
</div>
)}
<SidebarNav
sections={navItems}
onItemClick={() =>
setIsMobileMenuOpen(false)
}
/>
</div>
</div>
<div className="p-4 space-y-4 border-t shrink-0">
<SupporterStatus />
{env?.app?.version && (
<div className="text-xs text-muted-foreground text-center">
v{env.app.version}
</div>
)}
</div>
</SheetContent>
</Sheet>
</div>
)}
</div>
{showTopBar && (
<div className="ml-auto flex items-center justify-end">
<div className="flex items-center space-x-2">
<ThemeSwitcher />
<ProfileIcon />
</div>
</div>
)}
</div>
</div>
);
}
export default LayoutMobileMenu;

View file

@ -0,0 +1,178 @@
"use client";
import React, { useEffect, useState } from "react";
import { SidebarNav } from "@app/components/SidebarNav";
import { OrgSelector } from "@app/components/OrgSelector";
import { cn } from "@app/lib/cn";
import { ListUserOrgsResponse } from "@server/routers/org";
import SupporterStatus from "@app/components/SupporterStatus";
import { ExternalLink, Server, BookOpenText } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useUserContext } from "@app/hooks/useUserContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
import type { SidebarNavSection } from "@app/app/navigation";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@app/components/ui/tooltip";
interface LayoutSidebarProps {
orgId?: string;
orgs?: ListUserOrgsResponse["orgs"];
navItems: SidebarNavSection[];
defaultSidebarCollapsed: boolean;
}
export function LayoutSidebar({
orgId,
orgs,
navItems,
defaultSidebarCollapsed
}: LayoutSidebarProps) {
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(
defaultSidebarCollapsed
);
const pathname = usePathname();
const isAdminPage = pathname?.startsWith("/admin");
const { user } = useUserContext();
const { isUnlocked } = useLicenseStatusContext();
const { env } = useEnvContext();
const t = useTranslations();
const setSidebarStateCookie = (collapsed: boolean) => {
if (typeof window !== "undefined") {
const isSecure = window.location.protocol === "https:";
document.cookie = `pangolin-sidebar-state=${collapsed ? "collapsed" : "expanded"}; path=/; max-age=${60 * 60 * 24 * 30}; samesite=lax${isSecure ? "; secure" : ""}`;
}
};
useEffect(() => {
setSidebarStateCookie(isSidebarCollapsed);
}, [isSidebarCollapsed]);
return (
<div
className={cn(
"hidden md:flex border-r bg-card flex-col h-full shrink-0 relative",
isSidebarCollapsed ? "w-16" : "w-64"
)}
>
<div className="flex-1 overflow-y-auto">
<div className="p-4">
<OrgSelector
orgId={orgId}
orgs={orgs}
isCollapsed={isSidebarCollapsed}
/>
</div>
<div className="px-4 pt-1">
{!isAdminPage && user.serverAdmin && (
<div className="pb-4">
<Link
href="/admin"
className={cn(
"flex items-center rounded transition-colors text-muted-foreground hover:text-foreground text-sm w-full hover:bg-secondary/50 dark:hover:bg-secondary/20 rounded-md",
isSidebarCollapsed
? "px-2 py-2 justify-center"
: "px-3 py-1.5"
)}
title={
isSidebarCollapsed
? t("serverAdmin")
: undefined
}
>
<span
className={cn(
"flex-shrink-0",
!isSidebarCollapsed && "mr-2"
)}
>
<Server className="h-4 w-4" />
</span>
{!isSidebarCollapsed && (
<span>{t("serverAdmin")}</span>
)}
</Link>
</div>
)}
<SidebarNav
sections={navItems}
isCollapsed={isSidebarCollapsed}
/>
</div>
</div>
<div className="p-4 space-y-4 border-t shrink-0">
<SupporterStatus isCollapsed={isSidebarCollapsed} />
{!isSidebarCollapsed && (
<div className="space-y-2">
<div className="text-xs text-muted-foreground text-center">
<Link
href="https://github.com/fosrl/pangolin"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-1"
>
{!isUnlocked()
? t("communityEdition")
: t("commercialEdition")}
<ExternalLink size={12} />
</Link>
</div>
<div className="text-xs text-muted-foreground ">
<Link
href="https://docs.fossorial.io/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-1"
>
{t("documentation")}
<BookOpenText size={12} />
</Link>
</div>
{env?.app?.version && (
<div className="text-xs text-muted-foreground text-center">
v{env.app.version}
</div>
)}
</div>
)}
</div>
{/* Collapse button */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() =>
setIsSidebarCollapsed(!isSidebarCollapsed)
}
className="cursor-pointer absolute -right-2.5 top-1/2 transform -translate-y-1/2 w-2 h-8 rounded-full flex items-center justify-center transition-all duration-200 ease-in-out hover:scale-110 group"
aria-label={
isSidebarCollapsed
? "Expand sidebar"
: "Collapse sidebar"
}
>
<div className="w-0.5 h-4 bg-current opacity-30 group-hover:opacity-100 transition-opacity duration-200" />
</button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
<p>
{isSidebarCollapsed
? t("sidebarExpand")
: t("sidebarCollapse")}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}
export default LayoutSidebar;

View file

@ -1,5 +1,5 @@
import { useLocale } from "next-intl";
import LocaleSwitcherSelect from './LocaleSwitcherSelect';
import LocaleSwitcherSelect from "./LocaleSwitcherSelect";
export default function LocaleSwitcher() {
const locale = useLocale();
@ -10,44 +10,44 @@ export default function LocaleSwitcher() {
defaultValue={locale}
items={[
{
value: 'en-US',
label: 'English'
value: "en-US",
label: "English"
},
{
value: 'fr-FR',
value: "fr-FR",
label: "Français"
},
{
value: 'de-DE',
label: 'Deutsch'
value: "de-DE",
label: "Deutsch"
},
{
value: 'it-IT',
label: 'Italiano'
value: "it-IT",
label: "Italiano"
},
{
value: 'nl-NL',
label: 'Nederlands'
value: "nl-NL",
label: "Nederlands"
},
{
value: 'pl-PL',
label: 'Polski'
value: "pl-PL",
label: "Polski"
},
{
value: 'pt-PT',
label: 'Português'
value: "pt-PT",
label: "Português"
},
{
value: 'es-ES',
label: 'Español'
value: "es-ES",
label: "Español"
},
{
value: 'tr-TR',
label: 'Türkçe'
value: "tr-TR",
label: "Türkçe"
},
{
value: 'zh-CN',
label: '简体中文'
value: "zh-CN",
label: "简体中文"
}
]}
/>

View file

@ -1,17 +1,17 @@
'use client';
"use client";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@app/components/ui/dropdown-menu';
import { Button } from '@app/components/ui/button';
import { Check, Globe, Languages } from 'lucide-react';
import clsx from 'clsx';
import { useTransition } from 'react';
import { Locale } from '@/i18n/config';
import { setUserLocale } from '@/services/locale';
} from "@app/components/ui/dropdown-menu";
import { Button } from "@app/components/ui/button";
import { Check, Globe, Languages } from "lucide-react";
import clsx from "clsx";
import { useTransition } from "react";
import { Locale } from "@/i18n/config";
import { setUserLocale } from "@/services/locale";
type Props = {
defaultValue: string;
@ -41,8 +41,8 @@ export default function LocaleSwitcherSelect({
<Button
variant="ghost"
className={clsx(
'w-full rounded-sm h-8 gap-2 justify-start font-normal',
isPending && 'pointer-events-none'
"w-full rounded-sm h-8 gap-2 justify-start font-normal",
isPending && "pointer-events-none"
)}
aria-label={label}
>

View file

@ -15,10 +15,16 @@ import {
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@app/components/ui/tooltip";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { cn } from "@app/lib/cn";
import { ListUserOrgsResponse } from "@server/routers/org";
import { Check, ChevronsUpDown, Plus } from "lucide-react";
import { Check, ChevronsUpDown, Plus, Building2, Users } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useUserContext } from "@app/hooks/useUserContext";
@ -27,94 +33,110 @@ import { useTranslations } from "next-intl";
interface OrgSelectorProps {
orgId?: string;
orgs?: ListUserOrgsResponse["orgs"];
isCollapsed?: boolean;
}
export function OrgSelector({ orgId, orgs }: OrgSelectorProps) {
export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorProps) {
const { user } = useUserContext();
const [open, setOpen] = useState(false);
const router = useRouter();
const { env } = useEnvContext();
const t = useTranslations();
return (
const selectedOrg = orgs?.find((org) => org.orgId === orgId);
const orgSelectorContent = (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
size="lg"
variant="secondary"
size={isCollapsed ? "icon" : "lg"}
role="combobox"
aria-expanded={open}
className="w-full h-12 px-3 py-4 bg-neutral hover:bg-neutral"
className={cn(
"shadow-xs",
isCollapsed ? "w-8 h-8" : "w-full h-12 px-3 py-4"
)}
>
{isCollapsed ? (
<Building2 className="h-4 w-4" />
) : (
<div className="flex items-center justify-between w-full min-w-0">
<div className="flex items-center min-w-0 flex-1">
<Building2 className="h-4 w-4 mr-2 shrink-0" />
<div className="flex flex-col items-start min-w-0 flex-1">
<span className="font-bold text-sm">
{t('org')}
</span>
<span className="text-sm text-muted-foreground truncate w-full">
{orgId
? orgs?.find(
(org) =>
org.orgId ===
orgId
)?.name
: t('noneSelected')}
<span className="text-sm text-muted-foreground truncate w-full text-left">
{selectedOrg?.name || t('noneSelected')}
</span>
</div>
</div>
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50 ml-2" />
</div>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[280px] p-0">
<Command>
<CommandInput placeholder={t('searchProgress')} />
<CommandEmpty>
<PopoverContent className="w-[320px] p-0" align="start">
<Command className="rounded-lg">
<CommandInput
placeholder={t('searchProgress')}
className="border-0 focus:ring-0"
/>
<CommandEmpty className="py-6 text-center">
<div className="text-muted-foreground text-sm">
{t('orgNotFound2')}
</div>
</CommandEmpty>
{(!env.flags.disableUserCreateOrg ||
user.serverAdmin) && (
{(!env.flags.disableUserCreateOrg || user.serverAdmin) && (
<>
<CommandGroup heading={t('create')}>
<CommandGroup heading={t('create')} className="py-2">
<CommandList>
<CommandItem
onSelect={(
currentValue
) => {
router.push(
"/setup"
);
onSelect={() => {
setOpen(false);
router.push("/setup");
}}
className="mx-2 rounded-md"
>
<Plus className="mr-2 h-4 w-4" />
{t('setupNewOrg')}
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 mr-3">
<Plus className="h-4 w-4 text-primary" />
</div>
<div className="flex flex-col">
<span className="font-medium">{t('setupNewOrg')}</span>
<span className="text-xs text-muted-foreground">Create a new organization</span>
</div>
</CommandItem>
</CommandList>
</CommandGroup>
<CommandSeparator />
<CommandSeparator className="my-2" />
</>
)}
<CommandGroup heading={t('orgs')}>
<CommandGroup heading={t('orgs')} className="py-2">
<CommandList>
{orgs?.map((org) => (
<CommandItem
key={org.orgId}
onSelect={(
currentValue
) => {
router.push(
`/${org.orgId}/settings`
);
onSelect={() => {
setOpen(false);
router.push(`/${org.orgId}/settings`);
}}
className="mx-2 rounded-md"
>
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-muted mr-3">
<Users className="h-4 w-4 text-muted-foreground" />
</div>
<div className="flex flex-col flex-1">
<span className="font-medium">{org.name}</span>
<span className="text-xs text-muted-foreground">Organization</span>
</div>
<Check
className={cn(
"mr-2 h-4 w-4",
orgId === org.orgId
? "opacity-100"
: "opacity-0"
"h-4 w-4 text-primary",
orgId === org.orgId ? "opacity-100" : "opacity-0"
)}
/>
{org.name}
</CommandItem>
))}
</CommandList>
@ -123,4 +145,24 @@ export function OrgSelector({ orgId, orgs }: OrgSelectorProps) {
</PopoverContent>
</Popover>
);
if (isCollapsed) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
{orgSelectorContent}
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
<div className="text-center">
<p className="font-medium">{selectedOrg?.name || t('noneSelected')}</p>
<p className="text-xs text-muted-foreground">{t('org')}</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return orgSelectorContent;
}

View file

@ -23,10 +23,9 @@ import Disable2FaForm from "./Disable2FaForm";
import Enable2FaForm from "./Enable2FaForm";
import SupporterStatus from "./SupporterStatus";
import { UserType } from "@server/types/UserTypes";
import LocaleSwitcher from '@app/components/LocaleSwitcher';
import LocaleSwitcher from "@app/components/LocaleSwitcher";
import { useTranslations } from "next-intl";
export default function ProfileIcon() {
const { setTheme, theme } = useTheme();
const { env } = useEnvContext();
@ -57,10 +56,10 @@ export default function ProfileIcon() {
function logout() {
api.post("/auth/logout")
.catch((e) => {
console.error(t('logoutError'), e);
console.error(t("logoutError"), e);
toast({
title: t('logoutError'),
description: formatAxiosError(e, t('logoutError'))
title: t("logoutError"),
description: formatAxiosError(e, t("logoutError"))
});
})
.then(() => {
@ -74,10 +73,6 @@ export default function ProfileIcon() {
<Enable2FaForm open={openEnable2fa} setOpen={setOpenEnable2fa} />
<Disable2FaForm open={openDisable2fa} setOpen={setOpenDisable2fa} />
<div className="flex items-center md:gap-2 grow min-w-0 gap-2 md:gap-0">
<span className="truncate max-w-full font-medium min-w-0">
{user.email || user.name || user.username}
</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@ -89,15 +84,11 @@ export default function ProfileIcon() {
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56"
align="start"
forceMount
>
<DropdownMenuContent className="w-56" align="start" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{t('signingAs')}
{t("signingAs")}
</p>
<p className="text-xs leading-none text-muted-foreground">
{user.email || user.name || user.username}
@ -105,11 +96,11 @@ export default function ProfileIcon() {
</div>
{user.serverAdmin ? (
<p className="text-xs leading-none text-muted-foreground mt-2">
{t('serverAdmin')}
{t("serverAdmin")}
</p>
) : (
<p className="text-xs leading-none text-muted-foreground mt-2">
{user.idpName || t('idpNameInternal')}
{user.idpName || t("idpNameInternal")}
</p>
)}
</DropdownMenuLabel>
@ -120,14 +111,14 @@ export default function ProfileIcon() {
<DropdownMenuItem
onClick={() => setOpenEnable2fa(true)}
>
<span>{t('otpEnable')}</span>
<span>{t("otpEnable")}</span>
</DropdownMenuItem>
)}
{user.twoFactorEnabled && (
<DropdownMenuItem
onClick={() => setOpenDisable2fa(true)}
>
<span>{t('otpDisable')}</span>
<span>{t("otpDisable")}</span>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
@ -138,9 +129,7 @@ export default function ProfileIcon() {
(themeOption) => (
<DropdownMenuItem
key={themeOption}
onClick={() =>
handleThemeChange(themeOption)
}
onClick={() => handleThemeChange(themeOption)}
>
{themeOption === "light" && (
<Sun className="mr-2 h-4 w-4" />
@ -167,11 +156,10 @@ export default function ProfileIcon() {
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => logout()}>
{/* <LogOut className="mr-2 h-4 w-4" /> */}
<span>{t('logout')}</span>
<span>{t("logout")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
);
}

View file

@ -1,35 +1,45 @@
"use client";
import React, { useState, useEffect } from "react";
import React from "react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { cn } from "@app/lib/cn";
import { ChevronDown, ChevronRight } from "lucide-react";
import { useUserContext } from "@app/hooks/useUserContext";
import { Badge } from "@app/components/ui/badge";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useTranslations } from "next-intl";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@app/components/ui/tooltip";
export interface SidebarNavItem {
export type SidebarNavItem = {
href: string;
title: string;
icon?: React.ReactNode;
children?: SidebarNavItem[];
autoExpand?: boolean;
showProfessional?: boolean;
}
};
export type SidebarNavSection = {
heading: string;
items: SidebarNavItem[];
};
export interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
items: SidebarNavItem[];
sections: SidebarNavSection[];
disabled?: boolean;
onItemClick?: () => void;
isCollapsed?: boolean;
}
export function SidebarNav({
className,
items,
sections,
disabled = false,
onItemClick,
isCollapsed = false,
...props
}: SidebarNavProps) {
const pathname = usePathname();
@ -39,34 +49,8 @@ export function SidebarNav({
const resourceId = params.resourceId as string;
const userId = params.userId as string;
const clientId = params.clientId as string;
const [expandedItems, setExpandedItems] = useState<Set<string>>(() => {
const autoExpanded = new Set<string>();
function findAutoExpandedAndActivePath(
items: SidebarNavItem[],
parentHrefs: string[] = []
) {
items.forEach((item) => {
const hydratedHref = hydrateHref(item.href);
const currentPath = [...parentHrefs, hydratedHref];
if (item.autoExpand || pathname.startsWith(hydratedHref)) {
currentPath.forEach((href) => autoExpanded.add(href));
}
if (item.children) {
findAutoExpandedAndActivePath(item.children, currentPath);
}
});
}
findAutoExpandedAndActivePath(items);
return autoExpanded;
});
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
const { user } = useUserContext();
const t = useTranslations();
function hydrateHref(val: string): string {
@ -78,48 +62,27 @@ export function SidebarNav({
.replace("{clientId}", clientId);
}
function toggleItem(href: string) {
setExpandedItems((prev) => {
const newSet = new Set(prev);
if (newSet.has(href)) {
newSet.delete(href);
} else {
newSet.add(href);
}
return newSet;
});
}
const renderNavItem = (
item: SidebarNavItem,
hydratedHref: string,
isActive: boolean,
isDisabled: boolean
) => {
const tooltipText =
item.showProfessional && !isUnlocked()
? `${t(item.title)} (${t("licenseBadge")})`
: t(item.title);
function renderItems(items: SidebarNavItem[], level = 0) {
return items.map((item) => {
const hydratedHref = hydrateHref(item.href);
const isActive = pathname.startsWith(hydratedHref);
const hasChildren = item.children && item.children.length > 0;
const isExpanded = expandedItems.has(hydratedHref);
const indent = level * 28; // Base indent for each level
const isProfessional = item.showProfessional && !isUnlocked();
const isDisabled = disabled || isProfessional;
return (
<div key={hydratedHref}>
<div
className="flex items-center group"
style={{ marginLeft: `${indent}px` }}
>
<div
className={cn(
"flex items-center w-full transition-colors rounded-md",
isActive && level === 0 && "bg-primary/10"
)}
>
const itemContent = (
<Link
href={isProfessional ? "#" : hydratedHref}
href={isDisabled ? "#" : hydratedHref}
className={cn(
"flex items-center w-full px-3 py-2",
"flex items-center rounded transition-colors hover:bg-secondary/50 dark:hover:bg-secondary/20 rounded-md",
isCollapsed ? "px-2 py-2 justify-center" : "px-3 py-1.5",
isActive
? "text-primary font-medium"
: "text-muted-foreground group-hover:text-foreground",
isDisabled && "cursor-not-allowed"
: "text-muted-foreground hover:text-foreground",
isDisabled && "cursor-not-allowed opacity-60"
)}
onClick={(e) => {
if (isDisabled) {
@ -130,64 +93,78 @@ export function SidebarNav({
}}
tabIndex={isDisabled ? -1 : undefined}
aria-disabled={isDisabled}
>
<div
className={cn(
"flex items-center",
isDisabled && "opacity-60"
)}
>
{item.icon && (
<span className="mr-3">
<span
className={cn("flex-shrink-0", !isCollapsed && "mr-2")}
>
{item.icon}
</span>
)}
{t(item.title)}
</div>
{isProfessional && (
<Badge
variant="outlinePrimary"
className="ml-2"
>
{t('licenseBadge')}
{!isCollapsed && (
<>
<span>{t(item.title)}</span>
{item.showProfessional && !isUnlocked() && (
<Badge variant="outlinePrimary" className="ml-2">
{t("licenseBadge")}
</Badge>
)}
</>
)}
</Link>
{hasChildren && (
<button
onClick={() => toggleItem(hydratedHref)}
className="p-2 rounded-md text-muted-foreground hover:text-foreground cursor-pointer"
disabled={isDisabled}
>
{isExpanded ? (
<ChevronDown className="h-5 w-5" />
) : (
<ChevronRight className="h-5 w-5" />
)}
</button>
)}
</div>
</div>
{hasChildren && isExpanded && (
<div className="space-y-1 mt-1">
{renderItems(item.children || [], level + 1)}
</div>
)}
</div>
);
});
if (isCollapsed) {
return (
<TooltipProvider key={hydratedHref}>
<Tooltip>
<TooltipTrigger asChild>{itemContent}</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
<p>{tooltipText}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return (
<React.Fragment key={hydratedHref}>{itemContent}</React.Fragment>
);
};
return (
<nav
className={cn(
"flex flex-col space-y-2",
"flex flex-col gap-2 text-sm",
disabled && "pointer-events-none opacity-60",
className
)}
{...props}
>
{renderItems(items)}
{sections.map((section) => (
<div key={section.heading} className="mb-2">
{!isCollapsed && (
<div className="px-3 py-1 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
{section.heading}
</div>
)}
<div className="flex flex-col gap-1">
{section.items.map((item) => {
const hydratedHref = hydrateHref(item.href);
const isActive = pathname.startsWith(hydratedHref);
const isProfessional =
item.showProfessional && !isUnlocked();
const isDisabled = disabled || isProfessional;
return renderNavItem(
item,
hydratedHref,
isActive,
isDisabled || false
);
})}
</div>
</div>
))}
</nav>
);
}

View file

@ -1,35 +0,0 @@
"use client";
import { SidebarNav } from "@app/components/SidebarNav";
import React from "react";
interface SideBarSettingsProps {
children: React.ReactNode;
sidebarNavItems: Array<{
title: string;
href: string;
icon?: React.ReactNode;
}>;
disabled?: boolean;
limitWidth?: boolean;
}
export function SidebarSettings({
children,
sidebarNavItems,
disabled,
limitWidth
}: SideBarSettingsProps) {
return (
<div className="space-y-4">
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-6 lg:space-y-0">
<aside className="lg:w-1/5">
<SidebarNav items={sidebarNavItems} disabled={disabled} />
</aside>
<div className={`flex-1 ${limitWidth ? "lg:max-w-2xl" : ""} space-y-6`}>
{children}
</div>
</div>
</div>
);
}

View file

@ -9,6 +9,12 @@ import {
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@app/components/ui/tooltip";
import { Button } from "./ui/button";
import {
Credenza,
@ -46,11 +52,15 @@ import {
CardHeader,
CardTitle
} from "./ui/card";
import { Check, ExternalLink } from "lucide-react";
import { Check, ExternalLink, Heart } from "lucide-react";
import confetti from "canvas-confetti";
import { useTranslations } from "next-intl";
export default function SupporterStatus() {
interface SupporterStatusProps {
isCollapsed?: boolean;
}
export default function SupporterStatus({ isCollapsed = false }: SupporterStatusProps) {
const { supporterStatus, updateSupporterStatus } =
useSupporterStatusContext();
const [supportOpen, setSupportOpen] = useState(false);
@ -411,8 +421,27 @@ export default function SupporterStatus() {
</Credenza>
{supporterStatus?.visible ? (
isCollapsed ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
className="w-8 h-8"
onClick={() => {
setPurchaseOptionsOpen(true);
}}
>
<Heart className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
<p>{t('supportKeyBuy')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<Button
variant="outlinePrimary"
size="sm"
className="gap-2 w-full"
onClick={() => {
@ -421,6 +450,7 @@ export default function SupporterStatus() {
>
{t('supportKeyBuy')}
</Button>
)
) : null}
</>
);

View file

@ -0,0 +1,78 @@
"use client";
import { Button } from "@app/components/ui/button";
import { Laptop, Moon, Sun } from "lucide-react";
import { useTranslations } from "next-intl";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export default function ThemeSwitcher() {
const { setTheme, theme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const t = useTranslations();
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<Button variant="ghost" size="sm" className="h-8">
<Sun className="h-4 w-4 mr-2" />
Light
</Button>
);
}
function cycleTheme() {
const currentTheme = theme || "system";
if (currentTheme === "light") {
setTheme("dark");
} else if (currentTheme === "dark") {
setTheme("system");
} else {
setTheme("light");
}
}
function getThemeIcon() {
const currentTheme = theme || "system";
if (currentTheme === "light") {
return <Sun className="h-4 w-4" />;
} else if (currentTheme === "dark") {
return <Moon className="h-4 w-4" />;
} else {
// When theme is "system", show icon based on resolved theme
if (resolvedTheme === "light") {
return <Sun className="h-4 w-4" />;
} else if (resolvedTheme === "dark") {
return <Moon className="h-4 w-4" />;
} else {
// Fallback to laptop icon if resolvedTheme is not available
return <Laptop className="h-4 w-4" />;
}
}
}
function getThemeText() {
const currentTheme = theme || "system";
const translated = t(currentTheme);
return translated.charAt(0).toUpperCase() + translated.slice(1);
}
return (
<Button
variant="ghost"
size="sm"
className="h-8"
onClick={cycleTheme}
title={`Current theme: ${theme || "system"}. Click to cycle themes.`}
>
{getThemeIcon()}
<span className="ml-2">{getThemeText()}</span>
</Button>
);
}

View file

@ -173,7 +173,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
(maxTags !== undefined && maxTags < 0) ||
(props.minTags !== undefined && props.minTags < 0)
) {
console.warn(t('tagsWarnCannotBeLessThanZero'));
console.warn(t("tagsWarnCannotBeLessThanZero"));
// error
return null;
}
@ -197,22 +197,28 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
(option) => option.text === newTagText
)
) {
console.warn(t('tagsWarnNotAllowedAutocompleteOptions'));
console.warn(
t("tagsWarnNotAllowedAutocompleteOptions")
);
return;
}
if (validateTag && !validateTag(newTagText)) {
console.warn(t('tagsWarnInvalid'));
console.warn(t("tagsWarnInvalid"));
return;
}
if (minLength && newTagText.length < minLength) {
console.warn(t('tagWarnTooShort', {tagText: newTagText}));
console.warn(
t("tagWarnTooShort", { tagText: newTagText })
);
return;
}
if (maxLength && newTagText.length > maxLength) {
console.warn(t('tagWarnTooLong', {tagText: newTagText}));
console.warn(
t("tagWarnTooLong", { tagText: newTagText })
);
return;
}
@ -229,10 +235,12 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
setTags((prevTags) => [...prevTags, newTag]);
onTagAdd?.(newTagText);
} else {
console.warn(t('tagsWarnReachedMaxNumber'));
console.warn(t("tagsWarnReachedMaxNumber"));
}
} else {
console.warn(t('tagWarnDuplicate', {tagText: newTagText}));
console.warn(
t("tagWarnDuplicate", { tagText: newTagText })
);
}
});
setInputValue("");
@ -258,12 +266,12 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
}
if (minLength && newTagText.length < minLength) {
console.warn(t('tagWarnTooShort'));
console.warn(t("tagWarnTooShort"));
return;
}
if (maxLength && newTagText.length > maxLength) {
console.warn(t('tagWarnTooLong'));
console.warn(t("tagWarnTooLong"));
return;
}
@ -308,7 +316,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
}
if (minLength && newTagText.length < minLength) {
console.warn(t('tagWarnTooShort'));
console.warn(t("tagWarnTooShort"));
// error
return;
}
@ -316,7 +324,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
// Validate maxLength
if (maxLength && newTagText.length > maxLength) {
// error
console.warn(t('tagWarnTooLong'));
console.warn(t("tagWarnTooLong"));
return;
}
@ -489,7 +497,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
<div className="w-full">
<div
className={cn(
`flex flex-row flex-wrap items-center gap-1.5 p-1.5 w-full rounded-md border border-input bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 bg-transparent`,
`flex flex-row flex-wrap items-center gap-1.5 p-1.5 w-full rounded-md border border-input bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50`,
styleClasses?.inlineTagsContainer
)}
>
@ -536,7 +544,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
onBlur={handleInputBlur}
{...inputProps}
className={cn(
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit",
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
// className,
styleClasses?.input
)}
@ -622,7 +630,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
onBlur={handleInputBlur}
{...inputProps}
className={cn(
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit",
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
// className,
styleClasses?.input
)}
@ -710,7 +718,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
onBlur={handleInputBlur}
{...inputProps}
className={cn(
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit",
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
// className,
styleClasses?.input
)}
@ -791,7 +799,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
onBlur={handleInputBlur}
{...inputProps}
className={cn(
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit",
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
// className,
styleClasses?.input
)}
@ -834,7 +842,8 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
onBlur={handleInputBlur}
{...inputProps}
className={cn(
styleClasses?.input
styleClasses?.input,
"shadow-none inset-shadow-none"
// className
)}
autoComplete={
@ -908,7 +917,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
tags.length >= maxTags)
}
className={cn(
"border-0 w-full",
"border-0 w-full shadow-none inset-shadow-none",
styleClasses?.input
// className
)}

View file

@ -14,14 +14,13 @@ const alertVariants = cva(
"border-destructive/50 border bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive",
success:
"border-green-500/50 border bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500",
info:
"border-blue-500/50 border bg-blue-500/10 text-blue-500 dark:border-blue-400 [&>svg]:text-blue-500",
},
info: "border-blue-500/50 border bg-blue-500/10 text-blue-500 dark:border-blue-400 [&>svg]:text-blue-500"
}
},
defaultVariants: {
variant: "default",
},
},
variant: "default"
}
}
);
const Alert = React.forwardRef<
@ -45,7 +44,7 @@ const AlertTitle = React.forwardRef<
ref={ref}
className={cn(
"mb-1 font-medium leading-none tracking-tight",
className,
className
)}
{...props}
/>

View file

@ -6,35 +6,35 @@ import { cn } from "@app/lib/cn";
import { Loader2 } from "lucide-react";
const buttonVariants = cva(
"cursor-pointer inline-flex items-center justify-center rounded-full whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50",
"cursor-pointer inline-flex items-center justify-center whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90",
"bg-primary text-primary-foreground hover:bg-primary/90 shadow-2xs",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
"bg-destructive text-white dark:text-destructive-foreground hover:bg-destructive/90 shadow-2xs",
outline:
"border border-input bg-card hover:bg-accent hover:text-accent-foreground",
"border border-input bg-card hover:bg-accent hover:text-accent-foreground shadow-2xs",
outlinePrimary:
"border border-primary bg-card hover:bg-primary/10 text-primary",
"border border-primary bg-card hover:bg-primary/10 text-primary shadow-2xs",
secondary:
"bg-secondary border border-input border text-secondary-foreground hover:bg-secondary/80",
"bg-secondary border border-input border text-secondary-foreground hover:bg-secondary/80 shadow-2xs",
ghost: "hover:bg-accent hover:text-accent-foreground",
squareOutlinePrimary:
"border border-primary bg-card hover:bg-primary/10 text-primary rounded-md",
"border border-primary bg-card hover:bg-primary/10 text-primary rounded-md shadow-2xs",
squareOutline:
"border border-input bg-card hover:bg-accent hover:text-accent-foreground rounded-md",
"border border-input bg-card hover:bg-accent hover:text-accent-foreground rounded-md shadow-2xs",
squareDefault:
"bg-primary text-primary-foreground hover:bg-primary/90 rounded-md",
"bg-primary text-primary-foreground hover:bg-primary/90 rounded-md shadow-2xs",
text: "",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: "h-9 px-4 py-2",
default: "h-9 rounded-md px-3",
sm: "h-8 rounded-md px-3",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9"
icon: "h-9 w-9 rounded-md"
}
},
defaultVariants: {

View file

@ -1,146 +1,174 @@
"use client";
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { SearchIcon } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from "@/components/ui/dialog";
import { cn } from "@app/lib/cn";
import { Dialog, DialogContent } from "@/components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
ref={ref}
data-slot="command"
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
);
}
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string;
description?: string;
className?: string;
showCloseButton?: boolean;
}) {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn("overflow-hidden p-0", className)}
>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
data-slot="command-input"
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-base md:text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
));
);
}
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none rounded-md items-center px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
function CommandList({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<span
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
);
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
);
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
);
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
);
}
export {
Command,
@ -151,5 +179,5 @@ export {
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
CommandSeparator
};

View file

@ -93,8 +93,8 @@ export function DataTable<TData, TValue>({
return (
<div className="container mx-auto max-w-12xl">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<div className="flex items-center max-w-sm w-full relative mr-2">
<CardHeader className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0 pb-4">
<div className="flex items-center w-full sm:max-w-sm sm:mr-2 relative">
<Input
placeholder={searchPlaceholder}
value={globalFilter ?? ""}
@ -105,7 +105,7 @@ export function DataTable<TData, TValue>({
/>
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 sm:justify-end">
{onRefresh && (
<Button
variant="outline"

View file

@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-99 data-[state=open]:zoom-in-99 data-[state=closed]:slide-out-to-bottom-[5%] data-[state=open]:slide-in-from-bottom-[5%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:scale-95 data-[state=open]:scale-100 sm:rounded-lg",
className
)}
{...props}

View file

@ -2,199 +2,263 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@app/lib/cn";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal
data-slot="dropdown-menu-portal"
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
);
}
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
);
}
>(({ className, inset, ...props }, ref) => (
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group
data-slot="dropdown-menu-group"
{...props}
/>
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
ref={ref}
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
);
}
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
data-slot="dropdown-menu-checkbox-item"
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
);
}
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
ref={ref}
data-slot="dropdown-menu-radio-item"
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
);
}
>(({ className, inset, ...props }, ref) => (
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
ref={ref}
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
);
}
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
function DropdownMenuSeparator({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return (
<DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
);
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
DropdownMenuSubContent
};

View file

@ -5,15 +5,16 @@ import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues
} from "react-hook-form";
import { cn } from "@app/lib/cn";
import { Label } from "@/components/ui/label";
import { cn } from "@app/lib/cn";
const Form = FormProvider;
@ -44,8 +45,8 @@ const FormField = <
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
@ -72,47 +73,44 @@ const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";
}
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
}
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
ref={ref}
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
@ -123,32 +121,24 @@ const FormControl = React.forwardRef<
{...props}
/>
);
});
FormControl.displayName = "FormControl";
}
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
data-slot="form-description"
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
}
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
const body = error ? String(error?.message ?? "") : props.children;
if (!body) {
return null;
@ -156,16 +146,15 @@ const FormMessage = React.forwardRef<
return (
<p
ref={ref}
data-slot="form-message"
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
}
export {
useFormField,

View file

@ -2,47 +2,60 @@
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { Dot } from "lucide-react";
import { MinusIcon } from "lucide-react";
import { cn } from "@app/lib/cn";
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput> & { obscured?: boolean }
>(({ className, containerClassName, obscured = false, ...props }, ref) => (
function InputOTP({
className,
containerClassName,
obscured = false,
...props
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string;
obscured?: boolean;
}) {
return (
<OTPInput
ref={ref}
data-slot="input-otp"
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
"flex items-center gap-2 has-disabled:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
));
InputOTP.displayName = "InputOTP";
);
}
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} />
));
InputOTPGroup.displayName = "InputOTPGroup";
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center", className)}
{...props}
/>
);
}
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number; obscured?: boolean }
>(({ index, className, obscured = false, ...props }, ref) => {
function InputOTPSlot({
index,
className,
obscured = false,
...props
}: React.ComponentProps<"div"> & {
index: number;
obscured?: boolean;
}) {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
const { char, hasFakeCaret, isActive } =
inputOTPContext?.slots[index] ?? {};
return (
<div
ref={ref}
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-base md:text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className
)}
{...props}
@ -50,22 +63,19 @@ const InputOTPSlot = React.forwardRef<
{char && obscured ? "•" : char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
)}
</div>
);
});
InputOTPSlot.displayName = "InputOTPSlot";
}
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon />
</div>
));
InputOTPSeparator.displayName = "InputOTPSeparator";
);
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View file

@ -14,8 +14,11 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<div className="relative">
<input
type={showPassword ? "text" : "password"}
data-slot="input"
className={cn(
"flex h-9 w-full rounded-md border border-input bg-card px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base inset-shadow-2xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
ref={ref}
@ -38,8 +41,11 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
) : (
<input
type={type}
data-slot="input"
className={cn(
"flex h-9 w-full rounded-md border border-input bg-card px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base inset-shadow-2xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
ref={ref}

View file

@ -2,25 +2,22 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@app/lib/cn";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
);
}
export { Label };

View file

@ -2,39 +2,46 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@app/lib/cn";
const Popover = PopoverPrimitive.Root;
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
const PopoverTrigger = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<PopoverPrimitive.Trigger
ref={ref}
className={cn(className, "rounded-md")}
{...props}
/>
));
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
);
}
export { Popover, PopoverTrigger, PopoverContent };
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View file

@ -2,81 +2,65 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { cn } from "@app/lib/cn";
const Select = SelectPrimitive.Root;
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
const SelectGroup = SelectPrimitive.Group;
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
const SelectValue = SelectPrimitive.Value;
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
ref={ref}
data-slot="select-trigger"
data-size={size}
className={cn(
"flex h-9 w-full items-center justify-between border border-input bg-card px-3 py-2 text-base md:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
"rounded-md",
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 w-full",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
);
}
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
data-slot="select-content"
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
@ -89,7 +73,7 @@ const SelectContent = React.forwardRef<
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
@ -97,65 +81,110 @@ const SelectContent = React.forwardRef<
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
);
}
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
data-slot="select-label"
className={cn(
"text-muted-foreground px-2 py-1.5 text-xs",
className
)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
);
}
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
ref={ref}
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
);
}
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
data-slot="select-separator"
className={cn(
"bg-border pointer-events-none -mx-1 my-1 h-px",
className
)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
);
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectGroup,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue
};

View file

@ -1,29 +1,30 @@
"use client";
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "@app/lib/cn";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
"cursor-pointer peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0.5"
"cursor-pointer bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-3.5 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(112%-2px)] data-[state=unchecked]:translate-x-[calc(18%)]"
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
</SwitchPrimitive.Root>
);
}
export { Switch };

View file

@ -16,7 +16,7 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 right-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 md:max-w-[420px]",
"fixed bottom-0 left-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 md:max-w-[420px]",
className
)}
{...props}
@ -25,18 +25,18 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-3 pr-8 transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=open]:fade-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[swipe=end]:animate-out",
"shadow-sm group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-3 pr-8 transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=open]:fade-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[swipe=end]:animate-out",
{
variants: {
variant: {
default: "border bg-card text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
"destructive group border-destructive bg-destructive text-destructive-foreground"
}
},
defaultVariants: {
variant: "default",
},
variant: "default"
}
}
);
@ -112,9 +112,9 @@ const ToastDescription = React.forwardRef<
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
@ -125,5 +125,5 @@ export {
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
ToastAction
};

View file

@ -7,7 +7,7 @@ import {
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
ToastViewport
} from "@/components/ui/toast";
export function Toaster() {
@ -15,13 +15,21 @@ export function Toaster() {
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
{toasts.map(function ({
id,
title,
description,
action,
...props
}) {
return (
<Toast key={id} {...props} className="mt-2">
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
<ToastDescription>
{description}
</ToastDescription>
)}
</div>
{action}

View file

@ -0,0 +1,60 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@app/lib/cn";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };