From 9795776edf0cd73b132a6297b621cf7fc3a481cb Mon Sep 17 00:00:00 2001
From: blinko <blinkospace@gmail.com>
Date: Fri, 27 Dec 2024 13:50:18 +0800
Subject: [PATCH] feat: enhance note sharing functionality and update
 translations #368 #356 #362

- Added new fields to the notes model for sharing: shareEncryptedUrl, shareExpiryDate, shareMaxView, and shareViewCount.
- Implemented share note feature in the backend, allowing notes to be shared with optional password protection and expiration.
- Updated frontend components to support sharing functionality, including a share dialog and password verification.
- Enhanced user experience with new translations for sharing-related messages across multiple languages.
- Removed fallback-development.js as it is no longer needed.
---
 .../20241226141834_0_30_7/migration.sql       |   5 +
 prisma/schema.prisma                          |   4 +
 public/fallback-7ih1VBpDWHKhJ-2-n8ZvF.js      |   1 +
 public/fallback-development.js                |  28 --
 public/locales/ar/translation.json            |  20 +-
 public/locales/de/translation.json            |  20 +-
 public/locales/en/translation.json            |  20 +-
 public/locales/es/translation.json            |  20 +-
 public/locales/fr/translation.json            |  20 +-
 public/locales/ja/translation.json            |  20 +-
 public/locales/ko/translation.json            |  20 +-
 public/locales/pt/translation.json            |  20 +-
 public/locales/ru/translation.json            |  20 +-
 public/locales/tr/translation.json            |  20 +-
 public/locales/zh-TW/translation.json         |  20 +-
 public/locales/zh/translation.json            |  20 +-
 src/components/BlinkoRightClickMenu/index.tsx |  24 +-
 src/components/BlinkoShareDialog/index.tsx    | 265 ++++++++++++++++++
 src/lib/prismaZodType.ts                      |   4 +
 src/pages/share/[id].tsx                      | 114 +++++++-
 src/server/routers/note.ts                    | 118 +++++++-
 src/server/routers/public.ts                  |   1 -
 src/store/blinkoStore.tsx                     |   9 +
 23 files changed, 745 insertions(+), 68 deletions(-)
 create mode 100644 prisma/migrations/20241226141834_0_30_7/migration.sql
 create mode 100644 public/fallback-7ih1VBpDWHKhJ-2-n8ZvF.js
 delete mode 100644 public/fallback-development.js
 create mode 100644 src/components/BlinkoShareDialog/index.tsx

diff --git a/prisma/migrations/20241226141834_0_30_7/migration.sql b/prisma/migrations/20241226141834_0_30_7/migration.sql
new file mode 100644
index 00000000..15880bf0
--- /dev/null
+++ b/prisma/migrations/20241226141834_0_30_7/migration.sql
@@ -0,0 +1,5 @@
+-- AlterTable
+ALTER TABLE "notes" ADD COLUMN     "shareEncryptedUrl" VARCHAR,
+ADD COLUMN     "shareExpiryDate" TIMESTAMPTZ(6),
+ADD COLUMN     "shareMaxView" INTEGER DEFAULT 0,
+ADD COLUMN     "shareViewCount" INTEGER DEFAULT 0;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 3c0880b6..fdd95b1c 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -63,6 +63,10 @@ model notes {
   isTop         Boolean         @default(false)
   isReviewed    Boolean         @default(false)
   sharePassword String          @default("") @db.VarChar
+  shareEncryptedUrl String?     @db.VarChar
+  shareExpiryDate DateTime?     @db.Timestamptz(6)
+  shareMaxView Int?            @default(0)
+  shareViewCount Int?          @default(0)
   metadata      Json?           @db.Json
   accountId     Int?
   createdAt     DateTime        @default(now()) @db.Timestamptz(6)
diff --git a/public/fallback-7ih1VBpDWHKhJ-2-n8ZvF.js b/public/fallback-7ih1VBpDWHKhJ-2-n8ZvF.js
new file mode 100644
index 00000000..14deb87a
--- /dev/null
+++ b/public/fallback-7ih1VBpDWHKhJ-2-n8ZvF.js
@@ -0,0 +1 @@
+(()=>{"use strict";self.fallback=async e=>"document"===e.destination?caches.match("/offline",{ignoreSearch:!0}):Response.error()})();
\ No newline at end of file
diff --git a/public/fallback-development.js b/public/fallback-development.js
deleted file mode 100644
index 4ab7d9a6..00000000
--- a/public/fallback-development.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/******/ (() => { // webpackBootstrap
-/******/ 	"use strict";
-var __webpack_exports__ = {};
-
-
-self.fallback = async request => {
-  // https://developer.mozilla.org/en-US/docs/Web/API/RequestDestination
-  switch (request.destination) {
-    case 'document':
-      if (true) return caches.match("/offline", {
-        ignoreSearch: true
-      });
-    case 'image':
-      if (false) {}
-    case 'audio':
-      if (false) {}
-    case 'video':
-      if (false) {}
-    case 'font':
-      if (false) {}
-    case '':
-      if (false) {}
-    default:
-      return Response.error();
-  }
-};
-/******/ })()
-;
\ No newline at end of file
diff --git a/public/locales/ar/translation.json b/public/locales/ar/translation.json
index ad785d93..e1957b8a 100644
--- a/public/locales/ar/translation.json
+++ b/public/locales/ar/translation.json
@@ -398,5 +398,23 @@
   "offline": "غير متصل",
   "close-background-animation": "إغلاق الخلفية المتحركة",
   "custom-bg-tip": "انتقل إلى https://www.shadergradient.co/ لإنشاء خلفية التدرج الخاصة بك",
-  "custom-background-url": "الخلفية المخصصة"
+  "custom-background-url": "الخلفية المخصصة",
+  "share": "مشاركة",
+  "need-password-to-access": "مطلوب كلمة مرور للوصول",
+  "password-error": "خطأ في كلمة المرور",
+  "cancel-share": "إلغاء المشاركة",
+  "create-share": "إنشاء مشاركة",
+  "share-link": "حصة الرابط",
+  "set-access-password": "قم بتعيين كلمة مرور الوصول",
+  "protect-your-shared-content": "حافظ على محتواك المشترك",
+  "access-password": "كلمة مرور الوصول",
+  "select-date": "حدد التاريخ",
+  "expiry-time": "وقت انتهاء الصلاحية",
+  "select-expiry-time": "اختر وقت انتهاء الصلاحية",
+  "permanent-valid": "صالح دائما",
+  "7days-expiry": "انتهاء صلاحية بعد 7 أيام",
+  "custom-expiry": "انتهاء مخصص",
+  "30days-expiry": "انتهاء صلاحية خلال 30 يومًا",
+  "share-link-expired": "انتهت صلاحية الرابط المشاركة",
+  "share-link-expired-desc": "لقد انتهت صلاحية هذا المشاركة، يرجى الاتصال بالمسؤول لإعادة مشاركتها!"
 }
diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json
index effdfcff..f058afc0 100644
--- a/public/locales/de/translation.json
+++ b/public/locales/de/translation.json
@@ -384,5 +384,23 @@
   "offline": "Offline",
   "close-background-animation": "Schließen der Hintergrundanimation",
   "custom-bg-tip": "Gehe zu https://www.shadergradient.co/, um deinen eigenen Verlaufshintergrund zu erstellen.",
-  "custom-background-url": "Benutzerdefinierten Hintergrund"
+  "custom-background-url": "Benutzerdefinierten Hintergrund",
+  "share": "Teilen",
+  "need-password-to-access": "Passwortzugriff erforderlich",
+  "password-error": "Passwortfehler",
+  "cancel-share": "Stornieren Freigeben",
+  "create-share": "Erstellen Teilen",
+  "share-link": "Freigabelink",
+  "set-access-password": "Legen Sie das Zugriffspasswort fest",
+  "protect-your-shared-content": "Schütze deine gemeinsamen Inhalte.",
+  "access-password": "Zugriffspasswort",
+  "select-date": "Wählen Sie ein Datum aus",
+  "expiry-time": "Verfallszeit",
+  "select-expiry-time": "Wählen Sie das Ablaufdatum aus.",
+  "permanent-valid": "Dauerhaft gültig",
+  "7days-expiry": "7 Tage Ablauf",
+  "custom-expiry": "Benutzerdefiniertes Ablaufdatum",
+  "30days-expiry": "30 Tage Ablauf",
+  "share-link-expired": "Freigabelink abgelaufen",
+  "share-link-expired-desc": "Dieser Link ist abgelaufen. Bitte wenden Sie sich an den Administrator, um ihn erneut freizugeben!"
 }
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 1bae3069..60a66e9b 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -401,5 +401,23 @@
   "offline": "Offline",
   "close-background-animation": "Close Background Animation",
   "custom-background-url": "Custom Background",
-  "custom-bg-tip": "Go to https://www.shadergradient.co/ to create your own gradient background"
+  "custom-bg-tip": "Go to https://www.shadergradient.co/ to create your own gradient background",
+  "share": "Share",
+  "need-password-to-access": "Password access required",
+  "password-error": "Password error",
+  "create-share": "Create Share",
+  "cancel-share": "Cancel Share",
+  "share-link": "Share Link",
+  "set-access-password": "Set access password",
+  "protect-your-shared-content": "Protect your shared content",
+  "access-password": "Access password",
+  "select-date": "Select date",
+  "expiry-time": "Expiry Time",
+  "select-expiry-time": "Select Expiry Time",
+  "permanent-valid": "Permanent Valid",
+  "7days-expiry": "7 Days Expiry",
+  "30days-expiry": "30 Days Expiry",
+  "custom-expiry": "Custom Expiry",
+  "share-link-expired": "Share Link Expired",
+  "share-link-expired-desc": "This share has expired please contact the administrator to re-share!"
 }
diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json
index 4d12ddeb..b34af581 100644
--- a/public/locales/es/translation.json
+++ b/public/locales/es/translation.json
@@ -376,5 +376,23 @@
   "offline": "Sin conexión",
   "close-background-animation": "Cerrar Animación de Fondo",
   "custom-bg-tip": "Ve a https://www.shadergradient.co/ para crear tu propio fondo degradado.",
-  "custom-background-url": "Fondo personalizado"
+  "custom-background-url": "Fondo personalizado",
+  "share": "Compartir",
+  "need-password-to-access": "Se requiere contraseña para acceder.",
+  "password-error": "Error de contraseña",
+  "cancel-share": "Cancelar Compartir",
+  "create-share": "Crear Compartir",
+  "share-link": "Compartir enlace",
+  "set-access-password": "Establecer contraseña de acceso",
+  "protect-your-shared-content": "Proteja su contenido compartido",
+  "access-password": "Contraseña de acceso",
+  "select-date": "Seleccionar fecha",
+  "expiry-time": "Tiempo de expiración",
+  "select-expiry-time": "Seleccionar tiempo de vencimiento",
+  "permanent-valid": "Válido permanente",
+  "7days-expiry": "Caducidad de 7 días",
+  "custom-expiry": "Caducidad personalizada",
+  "30days-expiry": "Caducidad de 30 días.",
+  "share-link-expired": "Enlace compartido caducado",
+  "share-link-expired-desc": "¡Esta compartición ha caducado, por favor contacta al administrador para volver a compartir!"
 }
diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json
index 93b875d6..12a5da79 100644
--- a/public/locales/fr/translation.json
+++ b/public/locales/fr/translation.json
@@ -373,5 +373,23 @@
   "offline": "Hors ligne",
   "close-background-animation": "Animation d'arrière-plan proche",
   "custom-bg-tip": "Allez sur https://www.shadergradient.co/ pour créer votre propre arrière-plan dégradé.",
-  "custom-background-url": "Arrière-plan personnalisé"
+  "custom-background-url": "Arrière-plan personnalisé",
+  "share": "Partager",
+  "need-password-to-access": "Accès par mot de passe requis",
+  "password-error": "Erreur de mot de passe",
+  "cancel-share": "Annuler le partage",
+  "create-share": "Créer Partager",
+  "share-link": "Partager le lien",
+  "set-access-password": "Définir le mot de passe d'accès",
+  "protect-your-shared-content": "Protégez votre contenu partagé.",
+  "access-password": "Mot de passe d'accès",
+  "select-date": "Sélectionner la date",
+  "expiry-time": "Heure d'expiration",
+  "select-expiry-time": "Sélectionnez l'heure d'expiration",
+  "permanent-valid": "Valide en permanence",
+  "7days-expiry": "Expiration de 7 jours",
+  "custom-expiry": "Expiration personnalisée",
+  "30days-expiry": "30 jours d'expiration",
+  "share-link-expired": "Lien partagé expiré",
+  "share-link-expired-desc": "Cette part a expiré, veuillez contacter l'administrateur pour la re-partager !"
 }
diff --git a/public/locales/ja/translation.json b/public/locales/ja/translation.json
index 8d690120..6e29a308 100644
--- a/public/locales/ja/translation.json
+++ b/public/locales/ja/translation.json
@@ -358,5 +358,23 @@
   "offline": "オフライン",
   "close-background-animation": "背景アニメーションを閉じる",
   "custom-bg-tip": "https://www.shadergradient.co/ にアクセスして、独自のグラデーション背景を作成してください。",
-  "custom-background-url": "カスタム背景"
+  "custom-background-url": "カスタム背景",
+  "share": "共有",
+  "need-password-to-access": "パスワードアクセスが必要です",
+  "password-error": "パスワードエラー",
+  "cancel-share": "共有をキャンセル",
+  "create-share": "共有を作成する",
+  "share-link": "共有リンク",
+  "set-access-password": "アクセスパスワードを設定します",
+  "protect-your-shared-content": "共有コンテンツを保護します。",
+  "access-password": "アクセスパスワード",
+  "select-date": "日付を選択してください。",
+  "expiry-time": "有効期限",
+  "select-expiry-time": "有効期限を選択します",
+  "permanent-valid": "永久有効",
+  "7days-expiry": "有効期限7日間",
+  "custom-expiry": "カスタム有効期限",
+  "30days-expiry": "30日有効期限",
+  "share-link-expired": "共有リンクの期限切れ",
+  "share-link-expired-desc": "この共有は期限切れです。再共有するには管理者に連絡してください!"
 }
diff --git a/public/locales/ko/translation.json b/public/locales/ko/translation.json
index 05531f29..f467194c 100644
--- a/public/locales/ko/translation.json
+++ b/public/locales/ko/translation.json
@@ -381,5 +381,23 @@
   "offline": "오프라인.",
   "close-background-animation": "배경 애니메이션 닫기",
   "custom-bg-tip": "https://www.shadergradient.co/ 에 방문하여 자체 그라데이션 배경을 만드세요.",
-  "custom-background-url": "사용자 정의 배경"
+  "custom-background-url": "사용자 정의 배경",
+  "share": "공유하기",
+  "need-password-to-access": "비밀번호로 접근이 필요합니다.",
+  "password-error": "비밀번호 오류",
+  "cancel-share": "공유 취소",
+  "create-share": "공유 만들기",
+  "share-link": "공유 링크",
+  "set-access-password": "액세스 암호 설정",
+  "protect-your-shared-content": "공유된 콘텐츠를 보호하세요.",
+  "access-password": "액세스 비밀번호",
+  "select-date": "날짜 선택",
+  "expiry-time": "만료 시간",
+  "select-expiry-time": "만료 시간 선택",
+  "permanent-valid": "영구적 유효",
+  "7days-expiry": "7일 만료",
+  "custom-expiry": "사용자 정의 만료",
+  "30days-expiry": "30일 만료",
+  "share-link-expired": "공유 링크가 만료되었습니다.",
+  "share-link-expired-desc": "이 공유가 만료되었습니다. 다시 공유하려면 관리자에게 문의하세요!"
 }
diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json
index 3dacf030..31aca300 100644
--- a/public/locales/pt/translation.json
+++ b/public/locales/pt/translation.json
@@ -309,5 +309,23 @@
   "offline": "Offline",
   "close-background-animation": "Fechar Animação de Fundo",
   "custom-bg-tip": "Acesse https://www.shadergradient.co/ para criar seu próprio fundo de gradiente.",
-  "custom-background-url": "Fundo Personalizado"
+  "custom-background-url": "Fundo Personalizado",
+  "share": "Compartilhar",
+  "need-password-to-access": "Acesso com senha é necessário",
+  "password-error": "Erro de senha",
+  "cancel-share": "Cancelar Compartilhar",
+  "create-share": "Criar Compartilhar",
+  "share-link": "Compartilhar Link",
+  "set-access-password": "Definir senha de acesso",
+  "protect-your-shared-content": "Proteja seu conteúdo compartilhado",
+  "access-password": "Senha de acesso",
+  "select-date": "Selecionar data",
+  "expiry-time": "Tempo de validade",
+  "select-expiry-time": "Selecionar Tempo de Expiração",
+  "permanent-valid": "Válido permanentemente.",
+  "7days-expiry": "Validade de 7 dias",
+  "custom-expiry": "Validade personalizada",
+  "30days-expiry": "30 Dias de Validade",
+  "share-link-expired": "O link compartilhado expirou",
+  "share-link-expired-desc": "Esta partilha expirou, por favor entre em contato com o administrador para recriar a partilha!"
 }
diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json
index 1c3dde1f..d1d69aed 100644
--- a/public/locales/ru/translation.json
+++ b/public/locales/ru/translation.json
@@ -317,5 +317,23 @@
   "offline": "Офлайн",
   "close-background-animation": "Закрыть фоновую анимацию",
   "custom-bg-tip": "Перейдите на https://www.shadergradient.co/, чтобы создать свой собственный градиентный фон.",
-  "custom-background-url": "Пользовательский фон"
+  "custom-background-url": "Пользовательский фон",
+  "share": "Поделиться",
+  "need-password-to-access": "Требуется пароль для доступа",
+  "password-error": "Ошибка пароля",
+  "cancel-share": "Отменить публикацию",
+  "create-share": "Создать Совместно",
+  "share-link": "Поделиться ссылкой",
+  "set-access-password": "Установите пароль доступа",
+  "protect-your-shared-content": "Защищайте ваш общий контент",
+  "access-password": "Пароль доступа",
+  "select-date": "Выберите дату",
+  "expiry-time": "Время окончания",
+  "select-expiry-time": "Выберите время истечения.",
+  "permanent-valid": "Постоянно действительный",
+  "7days-expiry": "Истекает через 7 дней",
+  "custom-expiry": "Пользовательский срок действия",
+  "30days-expiry": "Срок действия 30 дней",
+  "share-link-expired": "Ссылка для обмена устарела",
+  "share-link-expired-desc": "Эта доля истекла, пожалуйста, свяжитесь с администратором для повторного обмена."
 }
diff --git a/public/locales/tr/translation.json b/public/locales/tr/translation.json
index 2cfbf357..fb220a6c 100644
--- a/public/locales/tr/translation.json
+++ b/public/locales/tr/translation.json
@@ -327,5 +327,23 @@
   "offline": "Çevrimdışı",
   "close-background-animation": "Arka Plan Animasyonunu Kapat",
   "custom-bg-tip": "Kendi gradyan arka planınızı oluşturmak için https://www.shadergradient.co/ adresine gidin.",
-  "custom-background-url": "Özel Arka Plan"
+  "custom-background-url": "Özel Arka Plan",
+  "share": "Paylaşmak",
+  "need-password-to-access": "Şifre erişimi gereklidir",
+  "password-error": "Şifre hatası",
+  "cancel-share": "Paylaşımı İptal Et",
+  "create-share": "Paylaşım Oluştur",
+  "share-link": "Paylaşma Bağlantısı",
+  "set-access-password": "Erişim şifresi ayarla",
+  "protect-your-shared-content": "Paylaşılan içeriğinizi koruyun",
+  "access-password": "Şifreye erişim",
+  "select-date": "Tarih seçiniz",
+  "expiry-time": "Son Kullanma Tarihi",
+  "select-expiry-time": "Son Kullanma Tarihini Seç",
+  "permanent-valid": "Sürekli Geçerli",
+  "7days-expiry": "7 Gün Son Kullanım Tarihi",
+  "custom-expiry": "Özel Son Kullanma Tarihi",
+  "30days-expiry": "30 Gün Son Kullanma Tarihi",
+  "share-link-expired": "Paylaşım Bağlantısı Süresi Doldu",
+  "share-link-expired-desc": "Bu paylaşımın süresi doldu, lütfen tekrar paylaşım yapabilmek için yöneticiyle iletişime geçin!"
 }
diff --git a/public/locales/zh-TW/translation.json b/public/locales/zh-TW/translation.json
index d8a5163e..d9e7f464 100644
--- a/public/locales/zh-TW/translation.json
+++ b/public/locales/zh-TW/translation.json
@@ -414,5 +414,23 @@
   "offline": "離線",
   "close-background-animation": "關閉背景動畫",
   "custom-bg-tip": "前往https://www.shadergradient.co/ 創建您自己的漸層背景。",
-  "custom-background-url": "自訂背景"
+  "custom-background-url": "自訂背景",
+  "share": "分享",
+  "need-password-to-access": "需要密碼訪問",
+  "password-error": "密碼錯誤",
+  "cancel-share": "取消分享",
+  "create-share": "創建分享",
+  "share-link": "分享連結",
+  "set-access-password": "設置存取密碼",
+  "protect-your-shared-content": "保護您的共享內容",
+  "access-password": "存取密碼",
+  "select-date": "選擇日期",
+  "expiry-time": "到期時間",
+  "select-expiry-time": "選擇到期時間",
+  "permanent-valid": "永久有效",
+  "7days-expiry": "7天到期",
+  "custom-expiry": "自訂到期",
+  "30days-expiry": "30天到期",
+  "share-link-expired": "分享連結已過期",
+  "share-link-expired-desc": "此分享已過期,請聯繫管理員重新分享!"
 }
diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json
index 3b288f3c..8c89469b 100644
--- a/public/locales/zh/translation.json
+++ b/public/locales/zh/translation.json
@@ -404,5 +404,23 @@
   "offline": "离线",
   "close-background-animation": "关闭背景动画",
   "custom-bg-tip": "访问 https://www.shadergradient.co/ 创建您自己的渐变背景",
-  "custom-background-url": "自定义背景"
+  "custom-background-url": "自定义背景",
+  "share": "分享",
+  "need-password-to-access": "密码访问已被要求",
+  "password-error": "密码错误",
+  "create-share": "创建分享",
+  "cancel-share": "取消分享",
+  "share-link": "分享链接",
+  "set-access-password": "设置访问密码",
+  "access-password": "访问密码",
+  "protect-your-shared-content": "保护您分享的内容",
+  "select-date": "选择日期",
+  "expiry-time": "过期时间",
+  "select-expiry-time": "选择到期时间",
+  "permanent-valid": "永久有效",
+  "7days-expiry": "7天到期",
+  "custom-expiry": "自定义到期",
+  "30days-expiry": "30天有效期",
+  "share-link-expired": "分享链接已过期",
+  "share-link-expired-desc": "这份共享已过期,请联系管理员重新共享!"
 }
diff --git a/src/components/BlinkoRightClickMenu/index.tsx b/src/components/BlinkoRightClickMenu/index.tsx
index 75a08c1a..2a360efc 100644
--- a/src/components/BlinkoRightClickMenu/index.tsx
+++ b/src/components/BlinkoRightClickMenu/index.tsx
@@ -17,6 +17,7 @@ import { AiStore } from "@/store/aiStore";
 import { FocusEditorFixMobile } from "../Common/Editor/editorUtils";
 import { parseAbsoluteToLocal } from "@internationalized/date";
 import i18n from "@/lib/i18n";
+import { BlinkoShareDialog } from "../BlinkoShareDialog";
 
 export const ShowEditTimeModel = () => {
   const blinko = RootStore.Get(BlinkoStore)
@@ -107,10 +108,23 @@ const handleTop = () => {
 
 const handlePublic = () => {
   const blinko = RootStore.Get(BlinkoStore)
-  blinko.upsertNote.call({
-    id: blinko.curSelectedNote?.id,
-    isShare: !blinko.curSelectedNote?.isShare
+  RootStore.Get(DialogStore).setData({
+    size: 'md' as any,
+    isOpen: true,
+    title: i18n.t('share'),
+    isDismissable: false,
+    content: <BlinkoShareDialog defaultSettings={{
+      shareUrl: blinko.curSelectedNote?.shareEncryptedUrl ? window.location.origin + '/share/' + blinko.curSelectedNote?.shareEncryptedUrl : undefined,
+      expiryDate: blinko.curSelectedNote?.shareExpiryDate ?? undefined,
+      password: blinko.curSelectedNote?.sharePassword ?? '',
+      isShare: blinko.curSelectedNote?.isShare
+    }} />
   })
+
+  // blinko.upsertNote.call({
+  //   id: blinko.curSelectedNote?.id,
+  //   isShare: !blinko.curSelectedNote?.isShare
+  // })
 }
 
 const handleArchived = () => {
@@ -203,7 +217,7 @@ export const PublicItem = observer(() => {
   const blinko = RootStore.Get(BlinkoStore)
   return <div className="flex items-start gap-2">
     <Icon icon="ic:outline-share" width="20" height="20" />
-    <div>{blinko.curSelectedNote?.isShare ? t('unset-as-public') : t('set-as-public')}</div>
+    <div>{t('share')}</div>
   </div>
 })
 
@@ -332,7 +346,7 @@ export const LeftCickMenu = observer(({ onTrigger, className }: { onTrigger: ()
       <DropdownItem key="MutiSelectItem" onPress={() => {
         handleMultiSelect()
       }}><MutiSelectItem /></DropdownItem>
-        <DropdownItem key="EditTimeItem" onPress={() => ShowEditTimeModel()}> <EditTimeItem /></DropdownItem>
+      <DropdownItem key="EditTimeItem" onPress={() => ShowEditTimeModel()}> <EditTimeItem /></DropdownItem>
       <DropdownItem key="ConvertItem" onPress={ConvertItemFunction}> <ConvertItem /></DropdownItem>
       <DropdownItem key="TopItem" onPress={handleTop}> <TopItem />  </DropdownItem>
       <DropdownItem key="ShareItem" onPress={handlePublic}> <PublicItem />  </DropdownItem>
diff --git a/src/components/BlinkoShareDialog/index.tsx b/src/components/BlinkoShareDialog/index.tsx
new file mode 100644
index 00000000..4deaa40d
--- /dev/null
+++ b/src/components/BlinkoShareDialog/index.tsx
@@ -0,0 +1,265 @@
+import { observer } from "mobx-react-lite";
+import { Button, Card, Switch, Input, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Popover, PopoverTrigger, PopoverContent, InputOtp, Divider } from "@nextui-org/react";
+import { today, getLocalTimeZone, parseDate } from "@internationalized/date";
+import dayjs from "@/lib/dayjs";
+import { useState, useMemo } from "react";
+import { Icon } from "@iconify/react";
+import { Calendar } from "@nextui-org/react";
+import { motion, AnimatePresence } from "framer-motion";
+import { RootStore } from "@/store";
+import { BlinkoStore } from "@/store/blinkoStore";
+import { useTranslation } from "react-i18next";
+import { DialogStore } from "@/store/module/Dialog";
+
+interface ShareDialogProps {
+  defaultSettings: ShareSettings;
+  shareUrl?: string;
+}
+
+export interface ShareSettings {
+  expiryDate?: Date;
+  password?: string;
+  shareUrl?: string;
+  isShare?: boolean;
+}
+
+const expiryOptions = [
+  { key: "never", label: ("permanent-valid") },
+  { key: "7days", label: ("7days-expiry") },
+  { key: "30days", label: ("30days-expiry") },
+  { key: "custom", label: ("custom-expiry") },
+];
+
+const generateRandomPassword = () => {
+  return Math.floor(100000 + Math.random() * 900000).toString();
+};
+
+export const BlinkoShareDialog = observer(({ defaultSettings }: ShareDialogProps) => {
+  const { t } = useTranslation()
+  const [settings, setSettings] = useState<ShareSettings>(() => {
+    const initialPassword = defaultSettings.shareUrl ? defaultSettings.password : generateRandomPassword();
+    return {
+      ...defaultSettings,
+      password: initialPassword
+    };
+  });
+  const [expiryType, setExpiryType] = useState<string>(() => {
+    return defaultSettings.expiryDate ? "custom" : "never";
+  });
+  const [isPublic, setIsPublic] = useState<boolean>(defaultSettings.password ? false : true);
+  const [isShare, setIsShare] = useState<boolean>(defaultSettings.isShare ?? false);
+
+  const [shareUrl, setShareUrl] = useState<string>(defaultSettings?.shareUrl ?? '');
+  const [isCalendarOpen, setIsCalendarOpen] = useState(false);
+
+  const selectedExpiryValue = useMemo(() => {
+    if (expiryType === "never") return t("permanent-valid");
+    if (settings.expiryDate) {
+      return dayjs(settings.expiryDate).format('YYYY-MM-DD');
+    }
+    return t("select-expiry-time");
+  }, [expiryType, settings.expiryDate]);
+
+  const handleExpiryChange = (type: string) => {
+    setExpiryType(type);
+    if (type === "never") {
+      setSettings({ ...settings, expiryDate: undefined });
+    } else if (type === "7days") {
+      setSettings({
+        ...settings,
+        expiryDate: dayjs().add(7, 'days').toDate()
+      });
+    } else if (type === "30days") {
+      setSettings({
+        ...settings,
+        expiryDate: dayjs().add(30, 'days').toDate()
+      });
+    } else {
+      setSettings({
+        ...settings,
+        expiryDate: settings.expiryDate || dayjs().add(1, 'day').toDate()
+      });
+      setIsCalendarOpen(true);
+    }
+  };
+
+  return (
+    <Card shadow="none" className="flex flex-col gap-8 p-2 -mt-4">
+      <div className="flex flex-col gap-2">
+        <div className="flex items-center gap-2 ">
+          <span className="text-default-700 font-medium">{t("expiry-time")}</span>
+          <AnimatePresence mode="wait">
+            {settings.expiryDate && (
+              <motion.div
+                className="ml-auto bg-[#FEF4D5] text-sm text-[#F68C06] px-2 py-1 rounded-full flex items-center gap-2"
+                initial={{ x: -20, opacity: 0 }}
+                animate={{ x: 0, opacity: 1 }}
+                exit={{ x: -20, opacity: 0 }}
+                transition={{ duration: 0.3 }}
+              >
+                <Icon icon="lets-icons:clock" className="text-[#F68C06]" width="20" height="20" />
+                {dayjs(settings.expiryDate).fromNow()}
+              </motion.div>
+            )}
+          </AnimatePresence>
+        </div>
+
+        <div className="flex gap-2 mt-2">
+          <Dropdown>
+            <DropdownTrigger>
+              <Button
+                variant="bordered"
+                size="lg"
+                className="flex-1 justify-start "
+                startContent={<Icon icon="solar:calendar-bold" className="text-default-500" width="20" height="20" />}
+              >
+                {selectedExpiryValue}
+              </Button>
+            </DropdownTrigger>
+            <DropdownMenu
+              onAction={(key) => handleExpiryChange(key as string)}
+              selectedKeys={[expiryType]}
+            >
+              {expiryOptions.map((option) => (
+                <DropdownItem key={option.key}>
+                  {t(option.label)}
+                </DropdownItem>
+              ))}
+            </DropdownMenu>
+          </Dropdown>
+
+          {expiryType === "custom" && (
+            <Popover
+              isOpen={isCalendarOpen}
+              onOpenChange={setIsCalendarOpen}
+              placement="bottom-end"
+            >
+              <PopoverTrigger>
+                <Button
+                  variant="bordered"
+                  size="lg"
+                  startContent={<Icon icon="solar:calendar-mark-bold" className="text-default-500" width="20" height="20" />}
+                >
+                  {settings.expiryDate ? dayjs(settings.expiryDate).format('YYYY-MM-DD') : t("select-date")}
+                </Button>
+              </PopoverTrigger>
+              <PopoverContent>
+                <div className="p-1 bg-content1">
+                  <Calendar
+                    minValue={today(getLocalTimeZone())}
+                    value={settings.expiryDate ? parseDate(dayjs(settings.expiryDate).format('YYYY-MM-DD')) : today(getLocalTimeZone()).add({ days: 1 })}
+                    onChange={(date) => {
+                      if (date) {
+                        setSettings({
+                          ...settings,
+                          expiryDate: new Date(date.toString())
+                        });
+                        setIsCalendarOpen(false);
+                      }
+                    }}
+                  />
+                </div>
+              </PopoverContent>
+            </Popover>
+          )}
+        </div>
+      </div>
+
+      <div className="flex flex-col gap-2">
+        <div className="flex items-center gap-2">
+          <span className="text-default-700 font-medium">{t("access-password")}</span>
+          <span className="text-default-400 text-sm">{t("protect-your-shared-content")}</span>
+          <Switch
+            className="ml-auto"
+            size="sm"
+            isSelected={!isPublic}
+            onValueChange={(checked) => {
+              setIsPublic(!checked);
+              if (checked) {
+                setSettings({ ...settings, password: generateRandomPassword() });
+              }
+            }}
+          />
+        </div>
+        <div className="flex w-full justify-center items-center">
+          {!isPublic && (
+            <InputOtp
+              length={6}
+              placeholder={t("set-access-password")}
+              value={settings.password}
+              onValueChange={(value) => setSettings({ ...settings, password: value })}
+            />
+          )}
+        </div>
+      </div>
+
+      <Divider className="my-2" />
+
+
+      <AnimatePresence mode="wait">
+        {
+          shareUrl && (
+            <motion.div
+              className="flex flex-col gap-2"
+              initial={{ y: -20, opacity: 0 }}
+              animate={{ y: 0, opacity: 1 }}
+              exit={{ y: -20, opacity: 0 }}
+              transition={{ duration: 0.2 }}
+            >
+              <div className="flex items-center gap-2 mb-2">
+                <span className="text-default-700 font-medium">{t("share-link")}</span>
+              </div>
+              <div className="flex gap-2">
+                <Input
+                  variant="bordered"
+                  value={shareUrl}
+                  readOnly
+                  classNames={{
+                    input: "bg-default-50",
+                    inputWrapper: "bg-default-50"
+                  }}
+                />
+                <Button
+                  isIconOnly
+                  variant="flat"
+                  onPress={() => navigator.clipboard.writeText(shareUrl)}
+                >
+                  <Icon icon="solar:copy-bold" width="20" height="20" />
+                </Button>
+              </div>
+            </motion.div>
+          )
+        }
+      </AnimatePresence>
+
+      <div className="w-full flex items-end gap-4">
+        {
+          isShare && (
+            <Button variant="flat" className="w-full" onPress={() => {
+              RootStore.Get(BlinkoStore).shareNote.call({
+                id: RootStore.Get(BlinkoStore).curSelectedNote!.id!,
+                isCancel: true,
+              })
+              setIsShare(false)
+              RootStore.Get(DialogStore).close()
+            }}>
+              {t("cancel-share")}
+            </Button>
+          )
+        }
+        <Button color="primary" className="w-full" onPress={async () => {
+          const res = await RootStore.Get(BlinkoStore).shareNote.call({
+            id: RootStore.Get(BlinkoStore).curSelectedNote!.id!,
+            isCancel: false,
+            password: isPublic ? "" : settings.password,
+            expireAt: settings.expiryDate
+          })
+          setShareUrl(window.location.origin + '/share/' + (res?.shareEncryptedUrl ?? '') + (isPublic ? '' : '?password=' + (settings.password ?? '')))
+          setIsShare(true)
+        }}>
+          {t("create-share")}
+        </Button>
+      </div>
+    </Card>
+  );
+});
diff --git a/src/lib/prismaZodType.ts b/src/lib/prismaZodType.ts
index 5632f637..081d1b6d 100644
--- a/src/lib/prismaZodType.ts
+++ b/src/lib/prismaZodType.ts
@@ -68,6 +68,10 @@ export const notesSchema = z.object({
   isTop: z.boolean(),
   isReviewed: z.boolean(),
   sharePassword: z.string(),
+  shareEncryptedUrl: z.string().nullable().optional(),
+  shareExpiryDate: z.date().nullable().optional(),
+  shareMaxView: z.number().nullable().optional(),
+  shareViewCount: z.number().nullable().optional(),
   metadata: z.any(),
   accountId: z.union([z.number().int(), z.null()]),
   createdAt: z.coerce.date(),
diff --git a/src/pages/share/[id].tsx b/src/pages/share/[id].tsx
index 8ebb21ef..cc500e72 100644
--- a/src/pages/share/[id].tsx
+++ b/src/pages/share/[id].tsx
@@ -5,30 +5,59 @@ import { RootStore } from "@/store";
 import { PromiseState } from "@/store/standard/PromiseState";
 import { observer } from "mobx-react-lite";
 import { useRouter } from "next/router";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
 import { useMediaQuery } from "usehooks-ts";
 import VanillaTilt from 'vanilla-tilt';
 import dynamic from "next/dynamic";
+import { Card, InputOtp, Button } from "@nextui-org/react";
+import { Icon } from "@iconify/react";
+import { useTranslation } from "react-i18next";
 
 const GradientBackground = dynamic(
   () => import('@/components/Common/GradientBackground').then((mod) => mod.GradientBackground),
   { ssr: false }
 );
-const Page = observer(() => {
+
+const Page: React.FC = observer(() => {
   const isPc = useMediaQuery('(min-width: 768px)')
   const router = useRouter()
+  const { t } = useTranslation()
+  const [password, setPassword] = useState("");
+  const [error, setError] = useState(false);
+  const [hasPassword, setHasPassword] = useState(false);
+  const [isExpired, setIsExpired] = useState(false);
+
   const store = RootStore.Local(() => ({
     shareNote: new PromiseState({
-      function: async (id) => {
-        const notes = await api.notes.publicDetail.mutate({ id })
-        return notes
+      function: async (shareEncryptedUrl: string, password?: string) => {
+        try {
+          const notes = await api.notes.publicDetail.mutate({ shareEncryptedUrl, password })
+          if (notes.error === 'expired') {
+            setIsExpired(true);
+            return null;
+          }
+          if (notes.hasPassword && !password) {
+            setHasPassword(true);
+            return null;
+          }
+          return notes.data;
+        } catch (e) {
+          setError(true);
+          return null;
+        }
       }
     })
   }))
 
   useEffect(() => {
     if (!router.query.id) return
-    store.shareNote.call(Number(router.query.id))
+    const urlPassword = router.query.password as string;
+    if (urlPassword) {
+      setPassword(urlPassword);
+      store.shareNote.call(router.query.id as string, urlPassword);
+    } else {
+      store.shareNote.call(router.query.id as string);
+    }
   }, [router.isReady])
 
   useEffect(() => {
@@ -42,16 +71,71 @@ const Page = observer(() => {
     });
   }, [store.shareNote?.value]);
 
-  return <GradientBackground>
-    <div className='p-4 h-[100vh] w-full flex justify-center items-center' >
-      {
-        store.shareNote?.value &&
-        <div className="tilt-card glass-effect max-h-[90vh] overflow-y-scroll w-[95%] md:min-w-[30%] md:max-w-[50%] rounded-xl shadow-[1px_0_25px_11px_rgba(98,0,114,0.17)]">
-          <BlinkoCard blinkoItem={store.shareNote?.value} isShareMode glassEffect />
+  const handleVerify = () => {
+    setError(false);
+    store.shareNote.call(router.query.id as string, password);
+  };
+
+  if (isExpired) {
+    return (
+      <GradientBackground>
+        <div className='p-4 h-[100vh] w-full flex justify-center items-center'>
+          <Card className="p-6 flex flex-col gap-4 items-center glass-effect">
+            <div className="flex items-center gap-2">
+              <Icon icon="solar:clock-circle-bold" className="text-2xl text-danger" />
+              <span className="text-xl font-medium text-danger">{t("share-link-expired")}</span>
+            </div>
+            <p className="text-sm text-default-500">{t("share-link-expired-desc")}</p>
+          </Card>
         </div>
-      }
-    </div>
-  </GradientBackground>
+      </GradientBackground>
+    );
+  }
+
+  if (hasPassword && !store.shareNote?.value) {
+    return (
+      <GradientBackground>
+        <div className='p-4 h-[100vh] w-full flex justify-center items-center'>
+          <Card className="p-6 flex flex-col gap-4 items-center glass-effect">
+            <div className="flex items-center gap-2">
+              <Icon icon="solar:lock-password-bold" className="text-2xl" />
+              <span className="text-xl font-medium">{t("need-password-to-access")}</span>
+            </div>
+            <InputOtp
+              length={6}
+              value={password}
+              onValueChange={setPassword}
+              onComplete={handleVerify}
+              classNames={{
+                input: error ? "border-danger" : ""
+              }}
+            />
+            {error && <span className="text-danger text-sm">{t("password-error")}</span>}
+            <Button
+              color="primary"
+              className="w-full"
+              onPress={handleVerify}
+              isDisabled={password.length !== 6}
+            >
+              {t("verify")}
+            </Button>
+          </Card>
+        </div>
+      </GradientBackground>
+    );
+  }
+
+  return (
+    <GradientBackground>
+      <div className='p-4 h-[100vh] w-full flex justify-center items-center'>
+        {store.shareNote?.value && (
+          <div className="tilt-card glass-effect max-h-[90vh] overflow-y-scroll w-[95%] md:min-w-[30%] md:max-w-[50%] rounded-xl shadow-[1px_0_25px_11px_rgba(98,0,114,0.17)]">
+            <BlinkoCard blinkoItem={store.shareNote?.value} isShareMode glassEffect />
+          </div>
+        )}
+      </div>
+    </GradientBackground>
+  );
 });
 
 export default Page
\ No newline at end of file
diff --git a/src/server/routers/note.ts b/src/server/routers/note.ts
index 13ce1cf6..5b89b139 100644
--- a/src/server/routers/note.ts
+++ b/src/server/routers/note.ts
@@ -197,15 +197,63 @@ export const noteRouter = router({
   publicDetail: publicProcedure
     .meta({ openapi: { method: 'POST', path: '/v1/note/public-detail', summary: 'Query share note detail', tags: ['Note'] } })
     .input(z.object({
-      id: z.number(),
+      shareEncryptedUrl: z.string(),
+      password: z.string().optional()
+    }))
+    .output(z.object({
+      hasPassword: z.boolean(),
+      data: z.union([z.null(), notesSchema.merge(
+        z.object({
+          attachments: z.array(attachmentsSchema)
+        })
+      )]),
+      error: z.union([z.literal('expired'), z.null()]).default(null)
     }))
-    .output(z.union([z.null(), notesSchema.merge(
-      z.object({
-        attachments: z.array(attachmentsSchema)
-      }))]))
     .mutation(async function ({ input }) {
-      const { id } = input
-      return await prisma.notes.findFirst({ where: { id, isShare: true }, include: { tags: true, attachments: true }, })
+      const { shareEncryptedUrl, password } = input
+      const note = await prisma.notes.findFirst({
+        where: {
+          shareEncryptedUrl,
+          isShare: true
+        },
+        include: {
+          tags: true,
+          attachments: true
+        }
+      })
+
+      if (!note) {
+        return {
+          hasPassword: false,
+          data: null
+        }
+      }
+
+      if (note.shareExpiryDate && new Date() > note.shareExpiryDate) {
+        // throw new Error('Note expired')
+        return {
+          hasPassword: false,
+          data: null,
+          error: 'expired'
+        }
+      }
+
+      if (note.sharePassword) {
+        if (!password) {
+          return {
+            hasPassword: true,
+            data: null
+          }
+        }
+
+        if (password !== note.sharePassword) {
+          throw new Error('Password error')
+        }
+      }
+      return {
+        hasPassword: !!note.sharePassword,
+        data: note
+      }
     }),
   detail: authProcedure
     .meta({ openapi: { method: 'POST', path: '/v1/note/detail', summary: 'Query note detail', protect: true, tags: ['Note'] } })
@@ -452,6 +500,62 @@ export const noteRouter = router({
         }
       }
     }),
+
+  shareNote: authProcedure
+    .meta({ openapi: { method: 'POST', path: '/v1/note/share', summary: 'Share note', protect: true, tags: ['Note'] } })
+    .input(z.object({
+      id: z.number(),
+      isCancel: z.boolean().default(false),
+      password: z.string().optional(),
+      expireAt: z.date().optional()
+    }))
+    .output(notesSchema)
+    .mutation(async function ({ input, ctx }) {
+      const { id, isCancel, password, expireAt } = input;
+
+      const generateShareId = () => {
+        const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
+        let result = '';
+        for (let i = 0; i < 8; i++) {
+          result += chars.charAt(Math.floor(Math.random() * chars.length));
+        }
+        return result;
+      };
+
+      const note = await prisma.notes.findFirst({
+        where: {
+          id,
+          accountId: Number(ctx.id)
+        }
+      });
+
+      if (!note) {
+        throw new Error('Note not found');
+      }
+
+      if (isCancel) {
+        return await prisma.notes.update({
+          where: { id },
+          data: {
+            isShare: false,
+            sharePassword: "",
+            shareExpiryDate: null,
+            shareEncryptedUrl: null
+          }
+        });
+      } else {
+        const shareId = note.shareEncryptedUrl || generateShareId();
+        return await prisma.notes.update({
+          where: { id },
+          data: {
+            isShare: true,
+            shareEncryptedUrl: shareId,
+            sharePassword: password,
+            shareExpiryDate: expireAt
+          }
+        });
+      }
+    }),
   updateMany: authProcedure
     .meta({ openapi: { method: 'POST', path: '/v1/note/batch-update', summary: 'Batch update note', protect: true, tags: ['Note'] } })
     .input(z.object({
diff --git a/src/server/routers/public.ts b/src/server/routers/public.ts
index 380ea227..e2c654d3 100644
--- a/src/server/routers/public.ts
+++ b/src/server/routers/public.ts
@@ -165,7 +165,6 @@ export const publicRouter = router({
         }
 
         if (!spotifyClient) {
-
           spotifyClient = new SpotifyClient({
             consumer: {
               key: config.spotifyConsumerKey!,
diff --git a/src/store/blinkoStore.tsx b/src/store/blinkoStore.tsx
index d506cb3a..1fe46b2e 100644
--- a/src/store/blinkoStore.tsx
+++ b/src/store/blinkoStore.tsx
@@ -184,6 +184,15 @@ export class BlinkoStore implements Store {
     }
   })
 
+  shareNote = new PromiseState({
+    function: async (params: { id: number, isCancel: boolean, password?: string, expireAt?: Date }) => {
+      const res = await api.notes.shareNote.mutate(params)
+      RootStore.Get(ToastPlugin).success(i18n.t("operation-success"))
+      this.updateTicker++
+      return res
+    }
+  })
+
   async syncOfflineNotes() {
     if (!this.isOnline) return;