✨ feat(announcement): Popup and pinned
This commit is contained in:
parent
79621623d5
commit
f3680a7020
@ -5,7 +5,6 @@ import {
|
|||||||
deleteAnnouncement,
|
deleteAnnouncement,
|
||||||
getAnnouncementList,
|
getAnnouncementList,
|
||||||
updateAnnouncement,
|
updateAnnouncement,
|
||||||
updateAnnouncementEnable,
|
|
||||||
} from '@/services/admin/announcement';
|
} from '@/services/admin/announcement';
|
||||||
import { ConfirmButton } from '@repo/ui/confirm-button';
|
import { ConfirmButton } from '@repo/ui/confirm-button';
|
||||||
import { format } from '@shadcn/ui/lib/date-fns';
|
import { format } from '@shadcn/ui/lib/date-fns';
|
||||||
@ -51,16 +50,52 @@ export default function Page() {
|
|||||||
}}
|
}}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
accessorKey: 'enable',
|
accessorKey: 'show',
|
||||||
header: t('enable'),
|
header: t('show'),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={row.getValue('enable')}
|
defaultChecked={row.getValue('show')}
|
||||||
onCheckedChange={async (checked) => {
|
onCheckedChange={async (checked) => {
|
||||||
await updateAnnouncementEnable({
|
await updateAnnouncement({
|
||||||
id: row.original.id,
|
...row.original,
|
||||||
enable: checked,
|
show: checked,
|
||||||
|
});
|
||||||
|
ref.current?.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'pinned',
|
||||||
|
header: t('pinned'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={row.getValue('pinned')}
|
||||||
|
onCheckedChange={async (checked) => {
|
||||||
|
await updateAnnouncement({
|
||||||
|
...row.original,
|
||||||
|
pinned: checked,
|
||||||
|
});
|
||||||
|
ref.current?.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'popup',
|
||||||
|
header: t('popup'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={row.getValue('popup')}
|
||||||
|
onCheckedChange={async (checked) => {
|
||||||
|
await updateAnnouncement({
|
||||||
|
...row.original,
|
||||||
|
popup: checked,
|
||||||
});
|
});
|
||||||
ref.current?.refresh();
|
ref.current?.refresh();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Zadejte"
|
"titlePlaceholder": "Zadejte"
|
||||||
},
|
},
|
||||||
"hide": "skrýt",
|
"hide": "skrýt",
|
||||||
|
"pinned": "Připnuto",
|
||||||
|
"popup": "vyskakovací okno",
|
||||||
"show": "Zobrazit",
|
"show": "Zobrazit",
|
||||||
"title": "Titul",
|
"title": "Titul",
|
||||||
"updateSuccess": "Aktualizace byla úspěšná",
|
"updateSuccess": "Aktualizace byla úspěšná",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Bitte eingeben"
|
"titlePlaceholder": "Bitte eingeben"
|
||||||
},
|
},
|
||||||
"hide": "Verbergen",
|
"hide": "Verbergen",
|
||||||
|
"pinned": "Angeheftet",
|
||||||
|
"popup": "Popup",
|
||||||
"show": "Anzeigen",
|
"show": "Anzeigen",
|
||||||
"title": "Titel",
|
"title": "Titel",
|
||||||
"updateSuccess": "Aktualisierung erfolgreich",
|
"updateSuccess": "Aktualisierung erfolgreich",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Please enter"
|
"titlePlaceholder": "Please enter"
|
||||||
},
|
},
|
||||||
"hide": "Hide",
|
"hide": "Hide",
|
||||||
|
"pinned": "Pinned",
|
||||||
|
"popup": "Popup",
|
||||||
"show": "Show",
|
"show": "Show",
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
"updateSuccess": "Update Success",
|
"updateSuccess": "Update Success",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Por favor, introduce"
|
"titlePlaceholder": "Por favor, introduce"
|
||||||
},
|
},
|
||||||
"hide": "Ocultar",
|
"hide": "Ocultar",
|
||||||
|
"pinned": "Fijado",
|
||||||
|
"popup": "Ventana emergente",
|
||||||
"show": "mostrar",
|
"show": "mostrar",
|
||||||
"title": "Título",
|
"title": "Título",
|
||||||
"updateSuccess": "Actualización exitosa",
|
"updateSuccess": "Actualización exitosa",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Por favor, ingresa"
|
"titlePlaceholder": "Por favor, ingresa"
|
||||||
},
|
},
|
||||||
"hide": "Ocultar",
|
"hide": "Ocultar",
|
||||||
|
"pinned": "Fijado",
|
||||||
|
"popup": "ventana emergente",
|
||||||
"show": "Mostrar",
|
"show": "Mostrar",
|
||||||
"title": "Título",
|
"title": "Título",
|
||||||
"updateSuccess": "Actualización exitosa",
|
"updateSuccess": "Actualización exitosa",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Syötä"
|
"titlePlaceholder": "Syötä"
|
||||||
},
|
},
|
||||||
"hide": "piilota",
|
"hide": "piilota",
|
||||||
|
"pinned": "Kiinnitetty",
|
||||||
|
"popup": "ponnahdusikkuna",
|
||||||
"show": "Näytä",
|
"show": "Näytä",
|
||||||
"title": "Otsikko",
|
"title": "Otsikko",
|
||||||
"updateSuccess": "Päivitys onnistui",
|
"updateSuccess": "Päivitys onnistui",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Veuillez entrer"
|
"titlePlaceholder": "Veuillez entrer"
|
||||||
},
|
},
|
||||||
"hide": "Masquer",
|
"hide": "Masquer",
|
||||||
|
"pinned": "Épinglé",
|
||||||
|
"popup": "Fenêtre contextuelle",
|
||||||
"show": "afficher",
|
"show": "afficher",
|
||||||
"title": "Titre",
|
"title": "Titre",
|
||||||
"updateSuccess": "Mise à jour réussie",
|
"updateSuccess": "Mise à jour réussie",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "कृपया दर्ज करें"
|
"titlePlaceholder": "कृपया दर्ज करें"
|
||||||
},
|
},
|
||||||
"hide": "छिपाएं",
|
"hide": "छिपाएं",
|
||||||
|
"pinned": "पिन किया हुआ",
|
||||||
|
"popup": "पॉपअप",
|
||||||
"show": "प्रदर्शित करें",
|
"show": "प्रदर्शित करें",
|
||||||
"title": "शीर्षक",
|
"title": "शीर्षक",
|
||||||
"updateSuccess": "अपडेट सफल",
|
"updateSuccess": "अपडेट सफल",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Kérjük, adja meg"
|
"titlePlaceholder": "Kérjük, adja meg"
|
||||||
},
|
},
|
||||||
"hide": "elrejt",
|
"hide": "elrejt",
|
||||||
|
"pinned": "Kiemelt",
|
||||||
|
"popup": "Felugró ablak",
|
||||||
"show": "Megjelenítés",
|
"show": "Megjelenítés",
|
||||||
"title": "Cím",
|
"title": "Cím",
|
||||||
"updateSuccess": "Sikeres frissítés",
|
"updateSuccess": "Sikeres frissítés",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "入力してください"
|
"titlePlaceholder": "入力してください"
|
||||||
},
|
},
|
||||||
"hide": "隠す",
|
"hide": "隠す",
|
||||||
|
"pinned": "固定",
|
||||||
|
"popup": "ポップアップ",
|
||||||
"show": "表示",
|
"show": "表示",
|
||||||
"title": "タイトル",
|
"title": "タイトル",
|
||||||
"updateSuccess": "更新が成功しました",
|
"updateSuccess": "更新が成功しました",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "입력하세요"
|
"titlePlaceholder": "입력하세요"
|
||||||
},
|
},
|
||||||
"hide": "숨기기",
|
"hide": "숨기기",
|
||||||
|
"pinned": "고정",
|
||||||
|
"popup": "팝업",
|
||||||
"show": "표시",
|
"show": "표시",
|
||||||
"title": "제목",
|
"title": "제목",
|
||||||
"updateSuccess": "업데이트 성공",
|
"updateSuccess": "업데이트 성공",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Vennligst skriv inn"
|
"titlePlaceholder": "Vennligst skriv inn"
|
||||||
},
|
},
|
||||||
"hide": "skjul",
|
"hide": "skjul",
|
||||||
|
"pinned": "Festet",
|
||||||
|
"popup": "Popup",
|
||||||
"show": "Vis",
|
"show": "Vis",
|
||||||
"title": "Tittel",
|
"title": "Tittel",
|
||||||
"updateSuccess": "Oppdatering vellykket",
|
"updateSuccess": "Oppdatering vellykket",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Wprowadź"
|
"titlePlaceholder": "Wprowadź"
|
||||||
},
|
},
|
||||||
"hide": "ukryj",
|
"hide": "ukryj",
|
||||||
|
"pinned": "Przypięte",
|
||||||
|
"popup": "wyskakujące okienko",
|
||||||
"show": "Pokaż",
|
"show": "Pokaż",
|
||||||
"title": "Tytuł",
|
"title": "Tytuł",
|
||||||
"updateSuccess": "Aktualizacja zakończona pomyślnie",
|
"updateSuccess": "Aktualizacja zakończona pomyślnie",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Por favor, insira"
|
"titlePlaceholder": "Por favor, insira"
|
||||||
},
|
},
|
||||||
"hide": "ocultar",
|
"hide": "ocultar",
|
||||||
|
"pinned": "Fixado",
|
||||||
|
"popup": "Janela pop-up",
|
||||||
"show": "mostrar",
|
"show": "mostrar",
|
||||||
"title": "Título",
|
"title": "Título",
|
||||||
"updateSuccess": "Atualização bem-sucedida",
|
"updateSuccess": "Atualização bem-sucedida",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Introduceți"
|
"titlePlaceholder": "Introduceți"
|
||||||
},
|
},
|
||||||
"hide": "ascunde",
|
"hide": "ascunde",
|
||||||
|
"pinned": "Fixat",
|
||||||
|
"popup": "fereastră pop-up",
|
||||||
"show": "afișare",
|
"show": "afișare",
|
||||||
"title": "Titlu",
|
"title": "Titlu",
|
||||||
"updateSuccess": "Actualizare reușită",
|
"updateSuccess": "Actualizare reușită",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Пожалуйста, введите"
|
"titlePlaceholder": "Пожалуйста, введите"
|
||||||
},
|
},
|
||||||
"hide": "скрыть",
|
"hide": "скрыть",
|
||||||
|
"pinned": "Закреплено",
|
||||||
|
"popup": "всплывающее окно",
|
||||||
"show": "Показать",
|
"show": "Показать",
|
||||||
"title": "Заголовок",
|
"title": "Заголовок",
|
||||||
"updateSuccess": "Обновление успешно",
|
"updateSuccess": "Обновление успешно",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "กรุณาใส่"
|
"titlePlaceholder": "กรุณาใส่"
|
||||||
},
|
},
|
||||||
"hide": "ซ่อน",
|
"hide": "ซ่อน",
|
||||||
|
"pinned": "ปักหมุด",
|
||||||
|
"popup": "ป๊อปอัพ",
|
||||||
"show": "แสดง",
|
"show": "แสดง",
|
||||||
"title": "หัวข้อ",
|
"title": "หัวข้อ",
|
||||||
"updateSuccess": "อัปเดตสำเร็จ",
|
"updateSuccess": "อัปเดตสำเร็จ",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Lütfen giriniz"
|
"titlePlaceholder": "Lütfen giriniz"
|
||||||
},
|
},
|
||||||
"hide": "gizle",
|
"hide": "gizle",
|
||||||
|
"pinned": "Sabitlenmiş",
|
||||||
|
"popup": "açılır pencere",
|
||||||
"show": "göster",
|
"show": "göster",
|
||||||
"title": "Başlık",
|
"title": "Başlık",
|
||||||
"updateSuccess": "Güncelleme başarılı",
|
"updateSuccess": "Güncelleme başarılı",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Будь ласка, введіть"
|
"titlePlaceholder": "Будь ласка, введіть"
|
||||||
},
|
},
|
||||||
"hide": "приховати",
|
"hide": "приховати",
|
||||||
|
"pinned": "Закріплено",
|
||||||
|
"popup": "спливаюче вікно",
|
||||||
"show": "Показати",
|
"show": "Показати",
|
||||||
"title": "Заголовок",
|
"title": "Заголовок",
|
||||||
"updateSuccess": "Оновлення успішне",
|
"updateSuccess": "Оновлення успішне",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "Vui lòng nhập"
|
"titlePlaceholder": "Vui lòng nhập"
|
||||||
},
|
},
|
||||||
"hide": "Ẩn",
|
"hide": "Ẩn",
|
||||||
|
"pinned": "Ghim",
|
||||||
|
"popup": "Cửa sổ bật lên",
|
||||||
"show": "Hiển thị",
|
"show": "Hiển thị",
|
||||||
"title": "Tiêu đề",
|
"title": "Tiêu đề",
|
||||||
"updateSuccess": "Cập nhật thành công",
|
"updateSuccess": "Cập nhật thành công",
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
"deleteSuccess": "删除成功",
|
"deleteSuccess": "删除成功",
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
"editAnnouncement": "编辑公告",
|
"editAnnouncement": "编辑公告",
|
||||||
"enable": "启用",
|
"enable": "是否显示",
|
||||||
"form": {
|
"form": {
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"confirm": "确认",
|
"confirm": "确认",
|
||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "请输入"
|
"titlePlaceholder": "请输入"
|
||||||
},
|
},
|
||||||
"hide": "隐藏",
|
"hide": "隐藏",
|
||||||
|
"pinned": "置顶",
|
||||||
|
"popup": "弹窗",
|
||||||
"show": "显示",
|
"show": "显示",
|
||||||
"title": "标题",
|
"title": "标题",
|
||||||
"updateSuccess": "更新成功",
|
"updateSuccess": "更新成功",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
"titlePlaceholder": "請輸入"
|
"titlePlaceholder": "請輸入"
|
||||||
},
|
},
|
||||||
"hide": "隱藏",
|
"hide": "隱藏",
|
||||||
|
"pinned": "置頂",
|
||||||
|
"popup": "彈窗",
|
||||||
"show": "顯示",
|
"show": "顯示",
|
||||||
"title": "標題",
|
"title": "標題",
|
||||||
"updateSuccess": "更新成功",
|
"updateSuccess": "更新成功",
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
const config = [
|
const config = [
|
||||||
{
|
{
|
||||||
requestLibPath: "import request from '@/utils/request';",
|
requestLibPath: "import request from '@/utils/request';",
|
||||||
schemaPath: 'https://docs.ppanel.dev/swagger/common.json',
|
schemaPath:
|
||||||
|
'https://raw.githubusercontent.com/perfect-panel/ppanel-docs/refs/heads/main/public/swagger/common.json',
|
||||||
serversPath: './services',
|
serversPath: './services',
|
||||||
projectName: 'common',
|
projectName: 'common',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
requestLibPath: "import request from '@/utils/request';",
|
requestLibPath: "import request from '@/utils/request';",
|
||||||
schemaPath: 'https://docs.ppanel.dev/swagger/admin.json',
|
schemaPath:
|
||||||
|
'https://raw.githubusercontent.com/perfect-panel/ppanel-docs/refs/heads/main/public/swagger/admin.json',
|
||||||
serversPath: './services',
|
serversPath: './services',
|
||||||
projectName: 'admin',
|
projectName: 'admin',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -62,21 +62,6 @@ export async function getAnnouncement(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update announcement enable PUT /v1/admin/announcement/enable */
|
|
||||||
export async function updateAnnouncementEnable(
|
|
||||||
body: API.UpdateAnnouncementEnableRequest,
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<API.Response & { data?: any }>('/v1/admin/announcement/enable', {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: body,
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get announcement list GET /v1/admin/announcement/list */
|
/** Get announcement list GET /v1/admin/announcement/list */
|
||||||
export async function getAnnouncementList(
|
export async function getAnnouncementList(
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||||
|
|||||||
11
apps/admin/services/admin/typings.d.ts
vendored
11
apps/admin/services/admin/typings.d.ts
vendored
@ -11,8 +11,9 @@ declare namespace API {
|
|||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
enable: boolean;
|
show: boolean;
|
||||||
type: number;
|
pinned: boolean;
|
||||||
|
popup: boolean;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
@ -82,7 +83,6 @@ declare namespace API {
|
|||||||
type CreateAnnouncementRequest = {
|
type CreateAnnouncementRequest = {
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
type: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type CreateApplicationRequest = {
|
type CreateApplicationRequest = {
|
||||||
@ -823,8 +823,9 @@ declare namespace API {
|
|||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
enable: boolean;
|
show: boolean;
|
||||||
type: number;
|
pinned: boolean;
|
||||||
|
popup: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UpdateApplicationRequest = {
|
type UpdateApplicationRequest = {
|
||||||
|
|||||||
6
apps/admin/services/common/typings.d.ts
vendored
6
apps/admin/services/common/typings.d.ts
vendored
@ -3,8 +3,9 @@ declare namespace API {
|
|||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
enable: boolean;
|
show: boolean;
|
||||||
type: number;
|
pinned: boolean;
|
||||||
|
popup: boolean;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
@ -321,6 +322,7 @@ declare namespace API {
|
|||||||
server: number[];
|
server: number[];
|
||||||
show: boolean;
|
show: boolean;
|
||||||
sell: boolean;
|
sell: boolean;
|
||||||
|
sort: number;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Empty } from '@/components/empty';
|
|
||||||
import { queryAnnouncement } from '@/services/user/announcement';
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { Markdown } from '@repo/ui/markdown';
|
|
||||||
import { Card } from '@shadcn/ui/card';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
|
|
||||||
export default function Announcement() {
|
|
||||||
const t = useTranslations('dashboard');
|
|
||||||
|
|
||||||
const { data } = useQuery({
|
|
||||||
queryKey: ['queryAnnouncement', 1],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await queryAnnouncement({
|
|
||||||
page: 1,
|
|
||||||
size: 1,
|
|
||||||
});
|
|
||||||
return (data.data?.announcements?.[0] as API.Announcement) || {};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h2 className='flex items-center gap-1.5 font-semibold'>
|
|
||||||
<Icon icon='uil:bell' className='size-5' />
|
|
||||||
{t('latestAnnouncement')}
|
|
||||||
</h2>
|
|
||||||
<Card className='p-6'>
|
|
||||||
{data?.content ? <Markdown>{data?.content}</Markdown> : <Empty />}
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
281
apps/user/app/(main)/(user)/dashboard/content.tsx
Normal file
281
apps/user/app/(main)/(user)/dashboard/content.tsx
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Display } from '@/components/display';
|
||||||
|
import { queryApplicationConfig } from '@/services/user/subscribe';
|
||||||
|
import { queryUserSubscribe } from '@/services/user/user';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { getNextResetDate, isBrowser } from '@repo/ui/utils';
|
||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@shadcn/ui/accordion';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@shadcn/ui/alert-dialog';
|
||||||
|
import { Button } from '@shadcn/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@shadcn/ui/card';
|
||||||
|
import { differenceInDays } from '@shadcn/ui/lib/date-fns';
|
||||||
|
import { toast } from '@shadcn/ui/lib/sonner';
|
||||||
|
import { Separator } from '@shadcn/ui/separator';
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from '@shadcn/ui/tabs';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { QRCodeCanvas } from 'qrcode.react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import useGlobalStore from '@/config/use-global';
|
||||||
|
import { getStat } from '@/services/common/common';
|
||||||
|
import { getPlatform } from '@/utils/common';
|
||||||
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
|
import Renewal from '../order/renewal';
|
||||||
|
import ResetTraffic from '../order/reset-traffic';
|
||||||
|
import Subscribe from '../subscribe/page';
|
||||||
|
|
||||||
|
export default function Content() {
|
||||||
|
const t = useTranslations('dashboard');
|
||||||
|
const { getUserSubscribe, getAppSubLink } = useGlobalStore();
|
||||||
|
|
||||||
|
const [protocol, setProtocol] = useState('');
|
||||||
|
|
||||||
|
const { data: userSubscribe = [] } = useQuery({
|
||||||
|
queryKey: ['queryUserSubscribe'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await queryUserSubscribe();
|
||||||
|
return data.data?.list || [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { data: application } = useQuery({
|
||||||
|
queryKey: ['queryApplicationConfig'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await queryApplicationConfig();
|
||||||
|
return data.data as API.ApplicationResponse;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [platform, setPlatform] = useState<keyof API.ApplicationResponse>(getPlatform());
|
||||||
|
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ['getStat'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getStat({
|
||||||
|
skipErrorHandler: true,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{userSubscribe.length ? (
|
||||||
|
<>
|
||||||
|
<h2 className='flex items-center gap-1.5 font-semibold'>
|
||||||
|
<Icon icon='uil:servers' className='size-5' />
|
||||||
|
{t('mySubscriptions')}
|
||||||
|
</h2>
|
||||||
|
<div className='flex flex-wrap justify-between gap-4'>
|
||||||
|
<Tabs
|
||||||
|
value={platform}
|
||||||
|
onValueChange={(value) => setPlatform(value as keyof API.ApplicationResponse)}
|
||||||
|
className='w-full max-w-full md:w-auto'
|
||||||
|
>
|
||||||
|
<TabsList className='flex *:flex-auto'>
|
||||||
|
{application &&
|
||||||
|
Object.keys(application)?.map((item) => (
|
||||||
|
<TabsTrigger value={item} key={item} className='px-1 uppercase lg:px-3'>
|
||||||
|
{item}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
{data?.protocol && data?.protocol.length > 1 && (
|
||||||
|
<Tabs
|
||||||
|
value={protocol}
|
||||||
|
onValueChange={setProtocol}
|
||||||
|
className='w-full max-w-full md:w-auto'
|
||||||
|
>
|
||||||
|
<TabsList className='flex *:flex-auto'>
|
||||||
|
{['all', ...(data?.protocol || [])].map((item) => (
|
||||||
|
<TabsTrigger
|
||||||
|
value={item === 'all' ? '' : item}
|
||||||
|
key={item}
|
||||||
|
className='px-1 uppercase lg:px-3'
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{userSubscribe.map((item) => (
|
||||||
|
<Card key={item.id}>
|
||||||
|
<CardHeader className='flex flex-row flex-wrap items-center justify-between gap-2 space-y-0'>
|
||||||
|
<CardTitle className='font-medium'>{item.subscribe.name}</CardTitle>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button size='sm' variant='destructive'>
|
||||||
|
{t('resetSubscription')}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{t('prompt')}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t('confirmResetSubscription')}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={() => toast.success(t('resetSuccess'))}>
|
||||||
|
{t('confirm')}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
<ResetTraffic
|
||||||
|
id={item.subscribe_id}
|
||||||
|
token={item.token}
|
||||||
|
replacement={item.subscribe.replacement}
|
||||||
|
/>
|
||||||
|
<Renewal token={item.token} subscribe={item.subscribe} />
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ul className='grid grid-cols-2 gap-3 *:flex *:flex-col *:justify-between lg:grid-cols-4'>
|
||||||
|
<li>
|
||||||
|
<span className='text-muted-foreground'>{t('used')}</span>
|
||||||
|
<span className='text-2xl font-bold'>
|
||||||
|
<Display
|
||||||
|
type='traffic'
|
||||||
|
value={item.upload + item.download}
|
||||||
|
unlimited={!item.traffic}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className='text-muted-foreground'>{t('totalTraffic')}</span>
|
||||||
|
<span className='text-2xl font-bold'>
|
||||||
|
<Display type='traffic' value={item.traffic} unlimited={!item.traffic} />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className='text-muted-foreground'>{t('nextResetDays')}</span>
|
||||||
|
<span className='text-2xl font-semibold'>
|
||||||
|
{differenceInDays(getNextResetDate(item.start_time), new Date()) ||
|
||||||
|
t('unknown')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className='text-muted-foreground'>{t('expirationDays')}</span>
|
||||||
|
<span className='text-2xl font-semibold'>
|
||||||
|
{differenceInDays(new Date(item.expire_time), new Date()) || t('unknown')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Separator className='mt-4' />
|
||||||
|
<Accordion type='single' collapsible defaultValue='0' className='w-full'>
|
||||||
|
{getUserSubscribe(item.token, protocol)?.map((url, index) => (
|
||||||
|
<AccordionItem key={url} value={String(index)}>
|
||||||
|
<AccordionTrigger className='hover:no-underline'>
|
||||||
|
<div className='flex w-full flex-row items-center justify-between'>
|
||||||
|
<CardTitle className='text-sm font-medium'>
|
||||||
|
{t('subscriptionUrl')} {index + 1}
|
||||||
|
</CardTitle>
|
||||||
|
<CopyToClipboard
|
||||||
|
text={url}
|
||||||
|
onCopy={(text, result) => {
|
||||||
|
if (result) {
|
||||||
|
toast.success(t('copySuccess'));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className='text-primary hover:bg-accent mr-4 flex cursor-pointer rounded p-2 text-sm'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon='uil:copy' className='mr-2 size-5' />
|
||||||
|
{t('copy')}
|
||||||
|
</span>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className='grid grid-cols-3 gap-4 lg:grid-cols-4 xl:grid-cols-7'>
|
||||||
|
{application &&
|
||||||
|
application[platform]?.map((app) => (
|
||||||
|
<div
|
||||||
|
key={app.name}
|
||||||
|
className='text-muted-foreground flex size-full flex-col items-center justify-between gap-2 text-xs'
|
||||||
|
>
|
||||||
|
<span>{app.name}</span>
|
||||||
|
{app.icon && (
|
||||||
|
<Image src={app.icon} alt={app.name} width={50} height={50} />
|
||||||
|
)}
|
||||||
|
<div className='flex'>
|
||||||
|
<Button size='sm' variant='secondary' className='px-1.5' asChild>
|
||||||
|
<Link href={app.url!}>{t('download')}</Link>
|
||||||
|
</Button>
|
||||||
|
<CopyToClipboard
|
||||||
|
text={url}
|
||||||
|
onCopy={(text, result) => {
|
||||||
|
const href = getAppSubLink(app.subscribe_type, url);
|
||||||
|
if (isBrowser() && href) {
|
||||||
|
window.location.href = href;
|
||||||
|
} else if (result) {
|
||||||
|
toast.success(
|
||||||
|
<>
|
||||||
|
<p>{t('copySuccess')}</p>
|
||||||
|
<p>{t('manualImportMessage')}</p>
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button size='sm' className='p-2'>
|
||||||
|
{t('import')}
|
||||||
|
</Button>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className='text-muted-foreground hidden size-full flex-col items-center justify-between gap-2 text-sm lg:flex'>
|
||||||
|
<span>{t('qrCode')}</span>
|
||||||
|
<QRCodeCanvas
|
||||||
|
value={url}
|
||||||
|
size={80}
|
||||||
|
bgColor='transparent'
|
||||||
|
fgColor='rgb(59, 130, 246)'
|
||||||
|
/>
|
||||||
|
<span className='text-center'>{t('scanToSubscribe')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h2 className='flex items-center gap-1.5 font-semibold'>
|
||||||
|
<Icon icon='uil:shop' className='size-5' />
|
||||||
|
{t('purchaseSubscription')}
|
||||||
|
</h2>
|
||||||
|
<Subscribe />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,283 +1,12 @@
|
|||||||
'use client';
|
import Announcement from '@/components/announcement';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
import { Display } from '@/components/display';
|
import Content from './content';
|
||||||
import { queryApplicationConfig } from '@/services/user/subscribe';
|
|
||||||
import { queryUserSubscribe } from '@/services/user/user';
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { getNextResetDate, isBrowser } from '@repo/ui/utils';
|
|
||||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@shadcn/ui/accordion';
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from '@shadcn/ui/alert-dialog';
|
|
||||||
import { Button } from '@shadcn/ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@shadcn/ui/card';
|
|
||||||
import { differenceInDays } from '@shadcn/ui/lib/date-fns';
|
|
||||||
import { toast } from '@shadcn/ui/lib/sonner';
|
|
||||||
import { Separator } from '@shadcn/ui/separator';
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@shadcn/ui/tabs';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { QRCodeCanvas } from 'qrcode.react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import useGlobalStore from '@/config/use-global';
|
|
||||||
import { getStat } from '@/services/common/common';
|
|
||||||
import { getPlatform } from '@/utils/common';
|
|
||||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
|
||||||
import Renewal from '../order/renewal';
|
|
||||||
import ResetTraffic from '../order/reset-traffic';
|
|
||||||
import Subscribe from '../subscribe/page';
|
|
||||||
import Announcement from './announcemnet';
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
const t = useTranslations('dashboard');
|
|
||||||
const { getUserSubscribe, getAppSubLink } = useGlobalStore();
|
|
||||||
|
|
||||||
const [protocol, setProtocol] = useState('');
|
|
||||||
|
|
||||||
const { data: userSubscribe = [] } = useQuery({
|
|
||||||
queryKey: ['queryUserSubscribe'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await queryUserSubscribe();
|
|
||||||
return data.data?.list || [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { data: application } = useQuery({
|
|
||||||
queryKey: ['queryApplicationConfig'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await queryApplicationConfig();
|
|
||||||
return data.data as API.ApplicationResponse;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [platform, setPlatform] = useState<keyof API.ApplicationResponse>(getPlatform());
|
|
||||||
|
|
||||||
const { data } = useQuery({
|
|
||||||
queryKey: ['getStat'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getStat({
|
|
||||||
skipErrorHandler: true,
|
|
||||||
});
|
|
||||||
return data.data;
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-[calc(100vh-64px-58px-32px-114px)] w-full flex-col gap-4 overflow-hidden'>
|
<div className='flex min-h-[calc(100vh-64px-58px-32px-114px)] w-full flex-col gap-4 overflow-hidden'>
|
||||||
<Announcement />
|
<Announcement type='pinned' Authorization={(await cookies()).get('Authorization')?.value} />
|
||||||
{userSubscribe.length ? (
|
<Content />
|
||||||
<>
|
|
||||||
<h2 className='flex items-center gap-1.5 font-semibold'>
|
|
||||||
<Icon icon='uil:servers' className='size-5' />
|
|
||||||
{t('mySubscriptions')}
|
|
||||||
</h2>
|
|
||||||
<div className='flex flex-wrap justify-between gap-4'>
|
|
||||||
<Tabs
|
|
||||||
value={platform}
|
|
||||||
onValueChange={(value) => setPlatform(value as keyof API.ApplicationResponse)}
|
|
||||||
className='w-full max-w-full md:w-auto'
|
|
||||||
>
|
|
||||||
<TabsList className='flex *:flex-auto'>
|
|
||||||
{application &&
|
|
||||||
Object.keys(application)?.map((item) => (
|
|
||||||
<TabsTrigger value={item} key={item} className='px-1 uppercase lg:px-3'>
|
|
||||||
{item}
|
|
||||||
</TabsTrigger>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
{data?.protocol && data?.protocol.length > 1 && (
|
|
||||||
<Tabs
|
|
||||||
value={protocol}
|
|
||||||
onValueChange={setProtocol}
|
|
||||||
className='w-full max-w-full md:w-auto'
|
|
||||||
>
|
|
||||||
<TabsList className='flex *:flex-auto'>
|
|
||||||
{['all', ...(data?.protocol || [])].map((item) => (
|
|
||||||
<TabsTrigger
|
|
||||||
value={item === 'all' ? '' : item}
|
|
||||||
key={item}
|
|
||||||
className='px-1 uppercase lg:px-3'
|
|
||||||
>
|
|
||||||
{item}
|
|
||||||
</TabsTrigger>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{userSubscribe.map((item) => (
|
|
||||||
<Card key={item.id}>
|
|
||||||
<CardHeader className='flex flex-row flex-wrap items-center justify-between gap-2 space-y-0'>
|
|
||||||
<CardTitle className='font-medium'>{item.subscribe.name}</CardTitle>
|
|
||||||
<div className='flex gap-2'>
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button size='sm' variant='destructive'>
|
|
||||||
{t('resetSubscription')}
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>{t('prompt')}</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{t('confirmResetSubscription')}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={() => toast.success(t('resetSuccess'))}>
|
|
||||||
{t('confirm')}
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
<ResetTraffic
|
|
||||||
id={item.subscribe_id}
|
|
||||||
token={item.token}
|
|
||||||
replacement={item.subscribe.replacement}
|
|
||||||
/>
|
|
||||||
<Renewal token={item.token} subscribe={item.subscribe} />
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ul className='grid grid-cols-2 gap-3 *:flex *:flex-col *:justify-between lg:grid-cols-4'>
|
|
||||||
<li>
|
|
||||||
<span className='text-muted-foreground'>{t('used')}</span>
|
|
||||||
<span className='text-2xl font-bold'>
|
|
||||||
<Display
|
|
||||||
type='traffic'
|
|
||||||
value={item.upload + item.download}
|
|
||||||
unlimited={!item.traffic}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span className='text-muted-foreground'>{t('totalTraffic')}</span>
|
|
||||||
<span className='text-2xl font-bold'>
|
|
||||||
<Display type='traffic' value={item.traffic} unlimited={!item.traffic} />
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span className='text-muted-foreground'>{t('nextResetDays')}</span>
|
|
||||||
<span className='text-2xl font-semibold'>
|
|
||||||
{differenceInDays(getNextResetDate(item.start_time), new Date()) ||
|
|
||||||
t('unknown')}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span className='text-muted-foreground'>{t('expirationDays')}</span>
|
|
||||||
<span className='text-2xl font-semibold'>
|
|
||||||
{differenceInDays(new Date(item.expire_time), new Date()) || t('unknown')}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<Separator className='mt-4' />
|
|
||||||
<Accordion type='single' collapsible defaultValue='0' className='w-full'>
|
|
||||||
{getUserSubscribe(item.token, protocol)?.map((url, index) => (
|
|
||||||
<AccordionItem key={url} value={String(index)}>
|
|
||||||
<AccordionTrigger className='hover:no-underline'>
|
|
||||||
<div className='flex w-full flex-row items-center justify-between'>
|
|
||||||
<CardTitle className='text-sm font-medium'>
|
|
||||||
{t('subscriptionUrl')} {index + 1}
|
|
||||||
</CardTitle>
|
|
||||||
<CopyToClipboard
|
|
||||||
text={url}
|
|
||||||
onCopy={(text, result) => {
|
|
||||||
if (result) {
|
|
||||||
toast.success(t('copySuccess'));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className='text-primary hover:bg-accent mr-4 flex cursor-pointer rounded p-2 text-sm'
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon='uil:copy' className='mr-2 size-5' />
|
|
||||||
{t('copy')}
|
|
||||||
</span>
|
|
||||||
</CopyToClipboard>
|
|
||||||
</div>
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<div className='grid grid-cols-3 gap-4 lg:grid-cols-4 xl:grid-cols-7'>
|
|
||||||
{application &&
|
|
||||||
application[platform]?.map((app) => (
|
|
||||||
<div
|
|
||||||
key={app.name}
|
|
||||||
className='text-muted-foreground flex size-full flex-col items-center justify-between gap-2 text-xs'
|
|
||||||
>
|
|
||||||
<span>{app.name}</span>
|
|
||||||
{app.icon && (
|
|
||||||
<Image src={app.icon} alt={app.name} width={50} height={50} />
|
|
||||||
)}
|
|
||||||
<div className='flex'>
|
|
||||||
<Button size='sm' variant='secondary' className='px-1.5' asChild>
|
|
||||||
<Link href={app.url!}>{t('download')}</Link>
|
|
||||||
</Button>
|
|
||||||
<CopyToClipboard
|
|
||||||
text={url}
|
|
||||||
onCopy={(text, result) => {
|
|
||||||
const href = getAppSubLink(app.subscribe_type, url);
|
|
||||||
if (isBrowser() && href) {
|
|
||||||
window.location.href = href;
|
|
||||||
} else if (result) {
|
|
||||||
toast.success(
|
|
||||||
<>
|
|
||||||
<p>{t('copySuccess')}</p>
|
|
||||||
<p>{t('manualImportMessage')}</p>
|
|
||||||
</>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button size='sm' className='p-2'>
|
|
||||||
{t('import')}
|
|
||||||
</Button>
|
|
||||||
</CopyToClipboard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className='text-muted-foreground hidden size-full flex-col items-center justify-between gap-2 text-sm lg:flex'>
|
|
||||||
<span>{t('qrCode')}</span>
|
|
||||||
<QRCodeCanvas
|
|
||||||
value={url}
|
|
||||||
size={80}
|
|
||||||
bgColor='transparent'
|
|
||||||
fgColor='rgb(59, 130, 246)'
|
|
||||||
/>
|
|
||||||
<span className='text-center'>{t('scanToSubscribe')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
))}
|
|
||||||
</Accordion>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<h2 className='flex items-center gap-1.5 font-semibold'>
|
|
||||||
<Icon icon='uil:shop' className='size-5' />
|
|
||||||
{t('purchaseSubscription')}
|
|
||||||
</h2>
|
|
||||||
<Subscribe />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
import Announcement from '@/components/announcement';
|
||||||
import { SidebarInset, SidebarProvider } from '@shadcn/ui/sidebar';
|
import { SidebarInset, SidebarProvider } from '@shadcn/ui/sidebar';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
import { SidebarLeft } from './sidebar-left';
|
import { SidebarLeft } from './sidebar-left';
|
||||||
import { SidebarRight } from './sidebar-right';
|
import { SidebarRight } from './sidebar-right';
|
||||||
|
|
||||||
@ -8,6 +10,7 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
|||||||
<SidebarLeft className='sticky top-[84px] hidden w-52 border-r-0 bg-transparent lg:flex' />
|
<SidebarLeft className='sticky top-[84px] hidden w-52 border-r-0 bg-transparent lg:flex' />
|
||||||
<SidebarInset className='relative p-4'>{children}</SidebarInset>
|
<SidebarInset className='relative p-4'>{children}</SidebarInset>
|
||||||
<SidebarRight className='sticky top-[84px] hidden w-52 border-r-0 bg-transparent 2xl:flex' />
|
<SidebarRight className='sticky top-[84px] hidden w-52 border-r-0 bg-transparent 2xl:flex' />
|
||||||
|
<Announcement type='popup' Authorization={(await cookies()).get('Authorization')?.value} />
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
62
apps/user/components/announcement/index.tsx
Normal file
62
apps/user/components/announcement/index.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { queryAnnouncement } from '@/services/user/announcement';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { Markdown } from '@repo/ui/markdown';
|
||||||
|
import { Card } from '@shadcn/ui/card';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@shadcn/ui/dialog';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
import { Empty } from '../empty';
|
||||||
|
|
||||||
|
export default async function Announcement({
|
||||||
|
type,
|
||||||
|
Authorization,
|
||||||
|
}: {
|
||||||
|
type: 'popup' | 'pinned';
|
||||||
|
Authorization?: string;
|
||||||
|
}) {
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await queryAnnouncement(
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
size: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipErrorHandler: true,
|
||||||
|
Authorization,
|
||||||
|
},
|
||||||
|
).then((res) => {
|
||||||
|
return res.data.data?.announcements.find((item) => item[type]);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
const t = await getTranslations('dashboard');
|
||||||
|
|
||||||
|
if (type === 'popup') {
|
||||||
|
return (
|
||||||
|
<Dialog defaultOpen={!!data}>
|
||||||
|
<DialogContent className='sm:max-w-[425px]'>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{data?.title}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<Markdown>{data?.content}</Markdown>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'pinned') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2 className='flex items-center gap-1.5 font-semibold'>
|
||||||
|
<Icon icon='uil:bell' className='size-5' />
|
||||||
|
{t('latestAnnouncement')}
|
||||||
|
</h2>
|
||||||
|
<Card className='p-6'>
|
||||||
|
{data?.content ? <Markdown>{data?.content}</Markdown> : <Empty />}
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,15 @@
|
|||||||
const config = [
|
const config = [
|
||||||
{
|
{
|
||||||
requestLibPath: "import request from '@/utils/request';",
|
requestLibPath: "import request from '@/utils/request';",
|
||||||
schemaPath: 'https://docs.ppanel.dev/swagger/common.json',
|
schemaPath:
|
||||||
|
'https://raw.githubusercontent.com/perfect-panel/ppanel-docs/refs/heads/main/public/swagger/common.json',
|
||||||
serversPath: './services',
|
serversPath: './services',
|
||||||
projectName: 'common',
|
projectName: 'common',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
requestLibPath: "import request from '@/utils/request';",
|
requestLibPath: "import request from '@/utils/request';",
|
||||||
schemaPath: 'https://docs.ppanel.dev/swagger/user.json',
|
schemaPath:
|
||||||
|
'https://raw.githubusercontent.com/perfect-panel/ppanel-docs/refs/heads/main/public/swagger/user.json',
|
||||||
serversPath: './services',
|
serversPath: './services',
|
||||||
projectName: 'user',
|
projectName: 'user',
|
||||||
},
|
},
|
||||||
|
|||||||
6
apps/user/services/common/typings.d.ts
vendored
6
apps/user/services/common/typings.d.ts
vendored
@ -3,8 +3,9 @@ declare namespace API {
|
|||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
enable: boolean;
|
show: boolean;
|
||||||
type: number;
|
pinned: boolean;
|
||||||
|
popup: boolean;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
@ -321,6 +322,7 @@ declare namespace API {
|
|||||||
server: number[];
|
server: number[];
|
||||||
show: boolean;
|
show: boolean;
|
||||||
sell: boolean;
|
sell: boolean;
|
||||||
|
sort: number;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
|
|||||||
6
apps/user/services/user/typings.d.ts
vendored
6
apps/user/services/user/typings.d.ts
vendored
@ -3,8 +3,9 @@ declare namespace API {
|
|||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
enable: boolean;
|
show: boolean;
|
||||||
type: number;
|
pinned: boolean;
|
||||||
|
popup: boolean;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
@ -460,6 +461,7 @@ declare namespace API {
|
|||||||
server: number[];
|
server: number[];
|
||||||
show: boolean;
|
show: boolean;
|
||||||
sell: boolean;
|
sell: boolean;
|
||||||
|
sort: number;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user