mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
feat(shares): add shares page with search, table, and modals
Introduce a new shares page that includes a search bar, shares table, and various modals for managing shares. The page supports creating, editing, deleting, and generating links for shares. Additionally, it includes functionality for notifying recipients and viewing share details. The layout and components are designed to be reusable and maintainable.
This commit is contained in:
@@ -2,12 +2,13 @@ import { Metadata } from "next";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
import { getAllConfigs } from "@/http/endpoints";
|
import { getAllConfigs } from "@/http/endpoints";
|
||||||
|
import { Config } from "@/types/layout";
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
const response = await getAllConfigs();
|
const response = await getAllConfigs();
|
||||||
const appNameConfig = response.data.configs.find((config: any) => config.key === "appName");
|
const appNameConfig = response.data.configs.find((config: Config) => config.key === "appName");
|
||||||
const appName = appNameConfig?.value || "Palmr";
|
const appName = appNameConfig?.value || "Palmr";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -5,3 +5,8 @@ export interface HomeContentProps {
|
|||||||
export interface HomeHeaderProps {
|
export interface HomeHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
@@ -2,6 +2,7 @@ import { Metadata } from "next";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
import { getAllConfigs } from "@/http/endpoints";
|
import { getAllConfigs } from "@/http/endpoints";
|
||||||
|
import { Config } from "@/types/layout";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -11,7 +12,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
const response = await getAllConfigs();
|
const response = await getAllConfigs();
|
||||||
const appNameConfig = response.data.configs.find((config: any) => config.key === "appName");
|
const appNameConfig = response.data.configs.find((config: Config) => config.key === "appName");
|
||||||
const appName = appNameConfig?.value || "Palmr";
|
const appName = appNameConfig?.value || "Palmr";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -2,6 +2,7 @@ import { Metadata } from "next";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
import { getAllConfigs } from "@/http/endpoints";
|
import { getAllConfigs } from "@/http/endpoints";
|
||||||
|
import { Config } from "@/types/layout";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -11,7 +12,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
const response = await getAllConfigs();
|
const response = await getAllConfigs();
|
||||||
const appNameConfig = response.data.configs.find((config: any) => config.key === "appName");
|
const appNameConfig = response.data.configs.find((config: Config) => config.key === "appName");
|
||||||
const appName = appNameConfig?.value || "Palmr";
|
const appName = appNameConfig?.value || "Palmr";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -35,13 +35,14 @@ export default async function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
const locale = await getLocale();
|
const locale = await getLocale();
|
||||||
|
const isRTL = locale === "ar-SA";
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
useAppInfo.getState().refreshAppInfo();
|
useAppInfo.getState().refreshAppInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} suppressHydrationWarning>
|
<html lang={locale} dir={isRTL ? "rtl" : "ltr"} suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<Favicon />
|
<Favicon />
|
||||||
</head>
|
</head>
|
||||||
|
@@ -2,6 +2,7 @@ import { Metadata } from "next";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
import { getAllConfigs } from "@/http/endpoints";
|
import { getAllConfigs } from "@/http/endpoints";
|
||||||
|
import { Config } from "@/types/layout";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -11,7 +12,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
const response = await getAllConfigs();
|
const response = await getAllConfigs();
|
||||||
const appNameConfig = response.data.configs.find((config: any) => config.key === "appName");
|
const appNameConfig = response.data.configs.find((config: Config) => config.key === "appName");
|
||||||
const appName = appNameConfig?.value || "Palmr";
|
const appName = appNameConfig?.value || "Palmr";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -2,6 +2,7 @@ import { Metadata } from "next";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
import { getAllConfigs } from "@/http/endpoints";
|
import { getAllConfigs } from "@/http/endpoints";
|
||||||
|
import { Config } from "@/types/layout";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -11,7 +12,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
const response = await getAllConfigs();
|
const response = await getAllConfigs();
|
||||||
const appNameConfig = response.data.configs.find((config: any) => config.key === "appName");
|
const appNameConfig = response.data.configs.find((config: Config) => config.key === "appName");
|
||||||
const appName = appNameConfig?.value || "Palmr";
|
const appName = appNameConfig?.value || "Palmr";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -2,6 +2,7 @@ import { Metadata } from "next";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
import { getAllConfigs } from "@/http/endpoints";
|
import { getAllConfigs } from "@/http/endpoints";
|
||||||
|
import { Config } from "@/types/layout";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -11,7 +12,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
const response = await getAllConfigs();
|
const response = await getAllConfigs();
|
||||||
const appNameConfig = response.data.configs.find((config: any) => config.key === "appName");
|
const appNameConfig = response.data.configs.find((config: Config) => config.key === "appName");
|
||||||
const appName = appNameConfig?.value || "Palmr";
|
const appName = appNameConfig?.value || "Palmr";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
23
apps/app/src/app/shares/components/empty-shares-state.tsx
Normal file
23
apps/app/src/app/shares/components/empty-shares-state.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { IconPlus, IconShare } from "@tabler/icons-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface EmptySharesStateProps {
|
||||||
|
onCreateShare: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptySharesState({ onCreateShare }: EmptySharesStateProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-center py-6 flex flex-col items-center gap-2">
|
||||||
|
<IconShare className="w-8 h-8 text-gray-500" />
|
||||||
|
<p className="text-gray-500">{t("shares.empty.message")}</p>
|
||||||
|
<Button variant="default" size="sm" onClick={onCreateShare} className="gap-2">
|
||||||
|
<IconPlus className="h-4 w-4" />
|
||||||
|
{t("shares.empty.createButton")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
44
apps/app/src/app/shares/components/shares-header.tsx
Normal file
44
apps/app/src/app/shares/components/shares-header.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { IconLayoutDashboard, IconShare } from "@tabler/icons-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
|
export function SharesHeader() {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<IconShare className="text-xl" />
|
||||||
|
<h1 className="text-2xl font-bold">{t("shares.header.title")}</h1>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink asChild>
|
||||||
|
<Link href="/dashboard" className="flex items-center">
|
||||||
|
<IconLayoutDashboard size={20} className="mr-2" />
|
||||||
|
{t("common.dashboard")}
|
||||||
|
</Link>
|
||||||
|
</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator />
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<IconShare size={20} /> {t("shares.header.myShares")}
|
||||||
|
</span>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
48
apps/app/src/app/shares/components/shares-modals.tsx
Normal file
48
apps/app/src/app/shares/components/shares-modals.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { CreateShareModal } from "@/components/modals/create-share-modal";
|
||||||
|
import { GenerateShareLinkModal } from "@/components/modals/generate-share-link-modal";
|
||||||
|
import { ShareActionsModals } from "@/components/modals/share-actions-modals";
|
||||||
|
import { ShareDetailsModal } from "@/components/modals/share-details-modal";
|
||||||
|
import { SharesModalsProps } from "../types";
|
||||||
|
|
||||||
|
export function SharesModals({
|
||||||
|
isCreateModalOpen,
|
||||||
|
onCloseCreateModal,
|
||||||
|
shareToViewDetails,
|
||||||
|
shareToGenerateLink,
|
||||||
|
shareManager,
|
||||||
|
onSuccess,
|
||||||
|
onCloseViewDetails,
|
||||||
|
onCloseGenerateLink,
|
||||||
|
}: SharesModalsProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CreateShareModal isOpen={isCreateModalOpen} onClose={onCloseCreateModal} onSuccess={onSuccess} />
|
||||||
|
|
||||||
|
<ShareActionsModals
|
||||||
|
shareToDelete={shareManager.shareToDelete}
|
||||||
|
shareToEdit={shareManager.shareToEdit}
|
||||||
|
shareToManageFiles={shareManager.shareToManageFiles}
|
||||||
|
shareToManageRecipients={shareManager.shareToManageRecipients}
|
||||||
|
onCloseDelete={() => shareManager.setShareToDelete(null)}
|
||||||
|
onCloseEdit={() => shareManager.setShareToEdit(null)}
|
||||||
|
onCloseManageFiles={() => shareManager.setShareToManageFiles(null)}
|
||||||
|
onCloseManageRecipients={() => shareManager.setShareToManageRecipients(null)}
|
||||||
|
onDelete={shareManager.handleDelete}
|
||||||
|
onEdit={shareManager.handleEdit}
|
||||||
|
onManageFiles={shareManager.handleManageFiles}
|
||||||
|
onManageRecipients={shareManager.handleManageRecipients}
|
||||||
|
onSuccess={onSuccess}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ShareDetailsModal shareId={shareToViewDetails?.id || null} onClose={onCloseViewDetails} />
|
||||||
|
|
||||||
|
<GenerateShareLinkModal
|
||||||
|
share={shareToGenerateLink}
|
||||||
|
shareId={shareToGenerateLink?.id || null}
|
||||||
|
onClose={onCloseGenerateLink}
|
||||||
|
onGenerate={shareManager.handleGenerateLink}
|
||||||
|
onSuccess={onSuccess}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
48
apps/app/src/app/shares/components/shares-search.tsx
Normal file
48
apps/app/src/app/shares/components/shares-search.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { IconPlus, IconSearch } from "@tabler/icons-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { SharesSearchProps } from "../types";
|
||||||
|
|
||||||
|
export function SharesSearch({
|
||||||
|
searchQuery,
|
||||||
|
onSearchChange,
|
||||||
|
onCreateShare,
|
||||||
|
totalShares,
|
||||||
|
filteredCount,
|
||||||
|
}: SharesSearchProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h2 className="text-xl font-semibold">{t("shares.search.title")}</h2>
|
||||||
|
<Button onClick={onCreateShare}>
|
||||||
|
<IconPlus className="h-4 w-4" />
|
||||||
|
{t("shares.search.createButton")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative max-w-xs">
|
||||||
|
<IconSearch className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
className="pl-9"
|
||||||
|
placeholder={t("shares.search.placeholder")}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => onSearchChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{searchQuery && (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{t("shares.search.results", {
|
||||||
|
filtered: filteredCount,
|
||||||
|
total: totalShares,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
import { SharesTable } from "@/components/tables/shares-table";
|
||||||
|
import { SharesTableContainerProps } from "../types";
|
||||||
|
import { EmptySharesState } from "./empty-shares-state";
|
||||||
|
|
||||||
|
export function SharesTableContainer({ shares, onCopyLink, onCreateShare, shareManager }: SharesTableContainerProps) {
|
||||||
|
return shares.length > 0 ? (
|
||||||
|
<SharesTable
|
||||||
|
shares={shares}
|
||||||
|
onCopyLink={onCopyLink}
|
||||||
|
onDelete={shareManager.setShareToDelete}
|
||||||
|
onEdit={shareManager.setShareToEdit}
|
||||||
|
onGenerateLink={shareManager.setShareToGenerateLink}
|
||||||
|
onManageFiles={shareManager.setShareToManageFiles}
|
||||||
|
onManageRecipients={shareManager.setShareToManageRecipients}
|
||||||
|
onNotifyRecipients={shareManager.handleNotifyRecipients}
|
||||||
|
onViewDetails={shareManager.setShareToViewDetails}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EmptySharesState onCreateShare={onCreateShare} />
|
||||||
|
);
|
||||||
|
}
|
94
apps/app/src/app/shares/hooks/use-shares.ts
Normal file
94
apps/app/src/app/shares/hooks/use-shares.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { getAllConfigs, listUserShares, notifyRecipients } from "@/http/endpoints";
|
||||||
|
import { ListUserShares200SharesItem } from "@/http/models/listUserShares200SharesItem";
|
||||||
|
|
||||||
|
export function useShares() {
|
||||||
|
const t = useTranslations();
|
||||||
|
const [shares, setShares] = useState<ListUserShares200SharesItem[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [shareToViewDetails, setShareToViewDetails] = useState<ListUserShares200SharesItem | null>(null);
|
||||||
|
const [shareToGenerateLink, setShareToGenerateLink] = useState<ListUserShares200SharesItem | null>(null);
|
||||||
|
const [smtpEnabled, setSmtpEnabled] = useState("false");
|
||||||
|
|
||||||
|
const loadShares = async () => {
|
||||||
|
try {
|
||||||
|
const response = await listUserShares();
|
||||||
|
const allShares = response.data.shares || [];
|
||||||
|
const sortedShares = [...allShares].sort(
|
||||||
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
setShares(sortedShares);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(t("shares.errors.loadFailed"));
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadConfigs = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getAllConfigs();
|
||||||
|
const smtpConfig = response.data.configs.find((config: any) => config.key === "smtpEnabled");
|
||||||
|
|
||||||
|
setSmtpEnabled(smtpConfig?.value || "false");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(t("shares.errors.smtpConfigFailed"), error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadShares();
|
||||||
|
loadConfigs();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filteredShares = shares.filter(
|
||||||
|
(share) => share.name?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCopyLink = (share: ListUserShares200SharesItem) => {
|
||||||
|
if (!share.alias?.alias) return;
|
||||||
|
|
||||||
|
const link = `${window.location.origin}/s/${share.alias.alias}`;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(link);
|
||||||
|
toast.success(t("shares.messages.linkCopied"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNotifyRecipients = async (share: ListUserShares200SharesItem) => {
|
||||||
|
if (!share.alias?.alias) return;
|
||||||
|
|
||||||
|
const link = `${window.location.origin}/s/${share.alias.alias}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await notifyRecipients(share.id, { shareLink: link });
|
||||||
|
toast.success(t("shares.messages.recipientsNotified"));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(t("shares.errors.notifyFailed"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
shares,
|
||||||
|
isLoading,
|
||||||
|
searchQuery,
|
||||||
|
shareToViewDetails,
|
||||||
|
shareToGenerateLink,
|
||||||
|
filteredShares,
|
||||||
|
smtpEnabled,
|
||||||
|
setSearchQuery,
|
||||||
|
setShareToViewDetails,
|
||||||
|
setShareToGenerateLink,
|
||||||
|
handleCopyLink,
|
||||||
|
handleNotifyRecipients,
|
||||||
|
loadShares,
|
||||||
|
};
|
||||||
|
}
|
25
apps/app/src/app/shares/layout.tsx
Normal file
25
apps/app/src/app/shares/layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Metadata } from "next";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
|
import { getAllConfigs } from "@/http/endpoints";
|
||||||
|
import { Config } from "@/types/layout";
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
|
const t = await getTranslations();
|
||||||
|
|
||||||
|
const response = await getAllConfigs();
|
||||||
|
const appNameConfig = response.data.configs.find((config: Config) => config.key === "appName");
|
||||||
|
const appName = appNameConfig?.value || "Palmr";
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${t("shares.pageTitle")} | ${appName}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UsersManagementLayout({ children }: LayoutProps) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
82
apps/app/src/app/shares/page.tsx
Normal file
82
apps/app/src/app/shares/page.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { LoadingScreen } from "@/components/layout/loading-screen";
|
||||||
|
import { Navbar } from "@/components/layout/navbar";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { DefaultFooter } from "@/components/ui/default-footer";
|
||||||
|
import { useDisclosure } from "@/hooks/use-disclosure";
|
||||||
|
import { useShareManager } from "@/hooks/use-share-manager";
|
||||||
|
import { SharesHeader } from "./components/shares-header";
|
||||||
|
import { SharesModals } from "./components/shares-modals";
|
||||||
|
import { SharesSearch } from "./components/shares-search";
|
||||||
|
import { SharesTableContainer } from "./components/shares-table-container";
|
||||||
|
import { useShares } from "./hooks/use-shares";
|
||||||
|
|
||||||
|
export default function SharesPage() {
|
||||||
|
const {
|
||||||
|
shares,
|
||||||
|
isLoading,
|
||||||
|
searchQuery,
|
||||||
|
setSearchQuery,
|
||||||
|
filteredShares,
|
||||||
|
shareToViewDetails,
|
||||||
|
shareToGenerateLink,
|
||||||
|
handleCopyLink,
|
||||||
|
loadShares,
|
||||||
|
setShareToViewDetails,
|
||||||
|
setShareToGenerateLink,
|
||||||
|
smtpEnabled,
|
||||||
|
} = useShares();
|
||||||
|
|
||||||
|
const { isOpen: isCreateModalOpen, onOpen: onOpenCreateModal, onClose: onCloseCreateModal } = useDisclosure();
|
||||||
|
const shareManager = useShareManager(loadShares);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingScreen />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-screen flex flex-col">
|
||||||
|
<Navbar />
|
||||||
|
<div className="flex-1 max-w-7xl mx-auto w-full px-6 py-8">
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<SharesHeader />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<SharesSearch
|
||||||
|
filteredCount={filteredShares.length}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
totalShares={shares.length}
|
||||||
|
onCreateShare={onOpenCreateModal}
|
||||||
|
onSearchChange={setSearchQuery}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SharesTableContainer
|
||||||
|
shareManager={shareManager}
|
||||||
|
shares={filteredShares}
|
||||||
|
onCopyLink={handleCopyLink}
|
||||||
|
onCreateShare={onOpenCreateModal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<SharesModals
|
||||||
|
isCreateModalOpen={isCreateModalOpen}
|
||||||
|
shareManager={shareManager}
|
||||||
|
shareToGenerateLink={shareToGenerateLink}
|
||||||
|
shareToViewDetails={shareToViewDetails}
|
||||||
|
smtpEnabled={smtpEnabled}
|
||||||
|
onCloseCreateModal={onCloseCreateModal}
|
||||||
|
onCloseGenerateLink={() => setShareToGenerateLink(null)}
|
||||||
|
onCloseViewDetails={() => setShareToViewDetails(null)}
|
||||||
|
onSuccess={loadShares}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DefaultFooter />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
48
apps/app/src/app/shares/types/index.ts
Normal file
48
apps/app/src/app/shares/types/index.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { ListUserShares200SharesItem } from "@/http/models";
|
||||||
|
|
||||||
|
export interface SharesSearchProps {
|
||||||
|
searchQuery: string;
|
||||||
|
onSearchChange: (value: string) => void;
|
||||||
|
onCreateShare: () => void;
|
||||||
|
totalShares: number;
|
||||||
|
filteredCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharesTableContainerProps {
|
||||||
|
shares: any[];
|
||||||
|
onCopyLink: (share: any) => void;
|
||||||
|
onCreateShare: () => void;
|
||||||
|
shareManager: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShareManager {
|
||||||
|
shareToDelete: ListUserShares200SharesItem | null;
|
||||||
|
shareToEdit: ListUserShares200SharesItem | null;
|
||||||
|
shareToManageFiles: ListUserShares200SharesItem | null;
|
||||||
|
shareToManageRecipients: ListUserShares200SharesItem | null;
|
||||||
|
|
||||||
|
setShareToDelete: (share: ListUserShares200SharesItem | null) => void;
|
||||||
|
setShareToEdit: (share: ListUserShares200SharesItem | null) => void;
|
||||||
|
setShareToManageFiles: (share: ListUserShares200SharesItem | null) => void;
|
||||||
|
setShareToManageRecipients: (share: ListUserShares200SharesItem | null) => void;
|
||||||
|
setShareToViewDetails: (share: ListUserShares200SharesItem | null) => void;
|
||||||
|
setShareToGenerateLink: (share: ListUserShares200SharesItem | null) => void;
|
||||||
|
handleDelete: (shareId: string) => Promise<void>;
|
||||||
|
handleEdit: (shareId: string, data: any) => Promise<void>;
|
||||||
|
handleManageFiles: (shareId: string, files: any[]) => Promise<void>;
|
||||||
|
handleManageRecipients: (shareId: string, recipients: any[]) => Promise<void>;
|
||||||
|
handleGenerateLink: (shareId: string, alias: string) => Promise<void>;
|
||||||
|
handleNotifyRecipients: (share: ListUserShares200SharesItem) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharesModalsProps {
|
||||||
|
isCreateModalOpen: boolean;
|
||||||
|
onCloseCreateModal: () => void;
|
||||||
|
shareToViewDetails: any;
|
||||||
|
shareToGenerateLink: any;
|
||||||
|
shareManager: any;
|
||||||
|
onSuccess: () => void;
|
||||||
|
onCloseViewDetails: () => void;
|
||||||
|
onCloseGenerateLink: () => void;
|
||||||
|
smtpEnabled?: string;
|
||||||
|
}
|
@@ -2,6 +2,7 @@ import { Metadata } from "next";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
import { getAllConfigs } from "@/http/endpoints";
|
import { getAllConfigs } from "@/http/endpoints";
|
||||||
|
import { Config } from "@/types/layout";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -11,7 +12,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
const response = await getAllConfigs();
|
const response = await getAllConfigs();
|
||||||
const appNameConfig = response.data.configs.find((config: any) => config.key === "appName");
|
const appNameConfig = response.data.configs.find((config: Config) => config.key === "appName");
|
||||||
const appName = appNameConfig?.value || "Palmr";
|
const appName = appNameConfig?.value || "Palmr";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { IconArrowLeft, IconArrowRight, IconFile } from "@tabler/icons-react";
|
import { IconArrowLeft, IconArrowRight, IconFile } from "@tabler/icons-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { IconLanguage } from "@tabler/icons-react";
|
import { IconLanguage } from "@tabler/icons-react";
|
||||||
import { useLocale } from "next-intl";
|
import { useLocale } from "next-intl";
|
||||||
import { setCookie } from "nookies";
|
import { setCookie } from "nookies";
|
||||||
@@ -32,11 +32,18 @@ const languages = {
|
|||||||
const COOKIE_LANG_KEY = "NEXT_LOCALE";
|
const COOKIE_LANG_KEY = "NEXT_LOCALE";
|
||||||
const COOKIE_MAX_AGE = 365 * 24 * 60 * 60;
|
const COOKIE_MAX_AGE = 365 * 24 * 60 * 60;
|
||||||
|
|
||||||
|
const RTL_LANGUAGES = ["ar-SA"];
|
||||||
|
|
||||||
export function LanguageSwitcher() {
|
export function LanguageSwitcher() {
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const changeLanguage = (fullLocale: string) => {
|
const changeLanguage = (fullLocale: string) => {
|
||||||
|
// Update document direction based on language
|
||||||
|
const isRTL = RTL_LANGUAGES.includes(fullLocale);
|
||||||
|
document.documentElement.dir = isRTL ? "rtl" : "ltr";
|
||||||
|
|
||||||
setCookie(null, COOKIE_LANG_KEY, fullLocale, {
|
setCookie(null, COOKIE_LANG_KEY, fullLocale, {
|
||||||
maxAge: COOKIE_MAX_AGE,
|
maxAge: COOKIE_MAX_AGE,
|
||||||
path: "/",
|
path: "/",
|
||||||
|
@@ -40,7 +40,7 @@ export function Navbar() {
|
|||||||
<div className="flex flex-1 items-center justify-between">
|
<div className="flex flex-1 items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard" className="flex items-center gap-2 cursor-pointer">
|
<Link href="/dashboard" className="flex items-center gap-2 cursor-pointer">
|
||||||
{appLogo && <img alt={t("navbar.logoAlt")} className="h-8 w-8 object-contain" src={appLogo} />}
|
{appLogo && <img alt={t("navbar.logoAlt")} className="h-8 w-8 object-contain rounded" src={appLogo} />}
|
||||||
<p className="font-bold text-2xl">{appName}</p>
|
<p className="font-bold text-2xl">{appName}</p>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { IconCalendar, IconEye, IconLock, IconShare } from "@tabler/icons-react";
|
import { IconCalendar, IconEye, IconLock, IconShare } from "@tabler/icons-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { IconCopy } from "@tabler/icons-react";
|
import { IconCopy } from "@tabler/icons-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { IconLock, IconLockOpen, IconMail } from "@tabler/icons-react";
|
import { IconLock, IconLockOpen, IconMail } from "@tabler/icons-react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export function useDisclosure(initial = false) {
|
export function useDisclosure(initial = false) {
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
4
apps/app/src/types/layout.ts
Normal file
4
apps/app/src/types/layout.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface Config {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
Reference in New Issue
Block a user