diff --git a/dictionary/en.json b/dictionary/en.json
index b8f1d96..0e72cd7 100644
--- a/dictionary/en.json
+++ b/dictionary/en.json
@@ -3,6 +3,12 @@
"appName": "Fast3DText",
"editorName": "Editor"
},
+ "ErrorPage": {
+ "title": "Some thing went wrong",
+ "sorry": "Sorry!",
+ "tryAgain": "Try again",
+ "backToHome": "Back to Home"
+ },
"HomePage": {
"heroTitle": "Create Stunning 3D Text Designs",
"heroSubtitle": "Generate and customize beautiful 3D text effects for your projects",
@@ -46,7 +52,12 @@
"mouseRight": "mouse right for move",
"previewFullscreen": "Fullscreen Preview",
"downloadSize": "Size",
- "downloadBackground": "Download"
+ "downloadBackground": "Download",
+ "share": "Share Link",
+ "shareErrorNotSupportDesc": "Not Support Share Backgound Image At Now",
+ "shareSuccessDesc": "The link has been generated and is permanently valid",
+ "shareDialogClose": "Close",
+ "shareDialogCopyLink": "Copy Link"
},
"TextEditor": {
"seoTitle": "3D Text Generator - More Options",
diff --git a/dictionary/zh.json b/dictionary/zh.json
index 4c16a37..3eedfd8 100644
--- a/dictionary/zh.json
+++ b/dictionary/zh.json
@@ -3,6 +3,12 @@
"appName": "Fast3DText",
"editorName": "编辑器"
},
+ "ErrorPage": {
+ "title": " 错误页面",
+ "sorry": "对不起!",
+ "tryAgain": "再试一下",
+ "backToHome": "回到主页"
+ },
"HomePage": {
"heroTitle": "创建惊艳的3D文字设计",
"heroSubtitle": "为您的项目生成并定制精美的3D文字效果",
@@ -46,7 +52,12 @@
"mouseRight": "鼠标右键拖动移动",
"previewFullscreen": "全屏预览",
"downloadSize": "尺寸",
- "downloadBackground": "下载图片"
+ "downloadBackground": "下载图片",
+ "share": "分享链接",
+ "shareErrorNotSupportDesc": "分享时暂不支持图片背景",
+ "shareSuccessDesc": "链接已生成,永久有效",
+ "shareDialogClose": "关闭",
+ "shareDialogCopyLink": "复制链接"
},
"TextEditor": {
"seoTitle": "3D 文字生成器 - 更多的选项",
diff --git a/package.json b/package.json
index 5fad434..426b54e 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,9 @@
"@vercel/speed-insights": "^1.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "crypto-js": "^4.2.0",
"lucide-react": "^0.479.0",
+ "lz-string": "^1.5.0",
"motion": "^12.23.11",
"next": "15.2.4",
"next-intl": "^4.3.4",
@@ -30,6 +32,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.11",
+ "@types/crypto-js": "^4.2.2",
"@types/node": "^20.19.9",
"@types/react": "19.0.12",
"@types/react-dom": "19.0.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 53ab326..0e84e62 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,9 +23,15 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ crypto-js:
+ specifier: ^4.2.0
+ version: 4.2.0
lucide-react:
specifier: ^0.479.0
version: 0.479.0(react@19.0.0)
+ lz-string:
+ specifier: ^1.5.0
+ version: 1.5.0
motion:
specifier: ^12.23.11
version: 12.23.11(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -66,6 +72,9 @@ importers:
'@tailwindcss/postcss':
specifier: ^4.1.11
version: 4.1.11
+ '@types/crypto-js':
+ specifier: ^4.2.2
+ version: 4.2.2
'@types/node':
specifier: ^20.19.9
version: 20.19.9
@@ -1214,6 +1223,9 @@ packages:
'@tybys/wasm-util@0.10.0':
resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==}
+ '@types/crypto-js@4.2.2':
+ resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
+
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -1606,6 +1618,9 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
+ crypto-js@4.2.0:
+ resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
+
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
@@ -2240,6 +2255,10 @@ packages:
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
@@ -3959,6 +3978,8 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@types/crypto-js@4.2.2': {}
+
'@types/estree@1.0.8': {}
'@types/json-schema@7.0.15': {}
@@ -4342,6 +4363,8 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
+ crypto-js@4.2.0: {}
+
csstype@3.1.3: {}
damerau-levenshtein@1.0.8: {}
@@ -5110,6 +5133,8 @@ snapshots:
dependencies:
react: 19.0.0
+ lz-string@1.5.0: {}
+
magic-string@0.30.17:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.4
diff --git a/src/app/[locale]/editor/[data]/page.tsx b/src/app/[locale]/editor/[data]/page.tsx
new file mode 100644
index 0000000..20f0a93
--- /dev/null
+++ b/src/app/[locale]/editor/[data]/page.tsx
@@ -0,0 +1,23 @@
+import { decodeText } from "@/lib/utils";
+import { OnlyPage } from "../page";
+
+export default function Page({ params }: { params: { data: string } }) {
+
+ const data = params['data']
+
+ let backgroundProp, textProp
+
+ if (data) {
+ try {
+ const { bg, text } = JSON.parse(decodeText(data));
+
+ backgroundProp = bg;
+ textProp = text;
+ } catch (error) {
+ console.error("parse data from url error", error)
+ }
+ }
+
+ return ()
+
+}
\ No newline at end of file
diff --git a/src/app/[locale]/editor/page.tsx b/src/app/[locale]/editor/page.tsx
index 20ebddd..6b5ed62 100644
--- a/src/app/[locale]/editor/page.tsx
+++ b/src/app/[locale]/editor/page.tsx
@@ -1,28 +1,31 @@
+import { BackgroundProp } from "@/components/common/BackgroundSelector";
+import { TextProp } from "@/components/common/TextSetting";
import Footer from "@/components/Footer";
import FullEditor from "@/components/FullEditor";
import Header from "@/components/Header";
import { Locales } from "@/i18n/config";
-import { Flex } from "@radix-ui/themes";
+import { Box, Flex } from "@radix-ui/themes";
import { Metadata } from "next";
-import { getTranslations, setRequestLocale } from "next-intl/server";
-import { use } from "react";
+import { getTranslations } from "next-intl/server";
const host = process.env.NEXT_PUBLIC_HOST;
export default function Page() {
- return (
-
-
-
-
-
-
- );
+ return ()
}
+export function OnlyPage({ textProp, backgroundProp }: { textProp: TextProp | undefined, backgroundProp: BackgroundProp | undefined }) {
+ return
+
+
+
+
+
+ ;
+}
const locales = Locales;
export function generateStaticParams() {
diff --git a/src/app/error.tsx b/src/app/[locale]/error.tsx
similarity index 79%
rename from src/app/error.tsx
rename to src/app/[locale]/error.tsx
index 62989dd..d79c92c 100644
--- a/src/app/error.tsx
+++ b/src/app/[locale]/error.tsx
@@ -1,29 +1,25 @@
"use client";
import { ServerCrash } from "lucide-react";
-import { use, useEffect } from "react";
+import { useEffect } from "react";
import { Link } from "@/i18n/navigation";
import { useTranslations } from "next-intl";
-import { setRequestLocale } from "next-intl/server";
import { Button } from "@radix-ui/themes";
export default function Error({
- params,
error,
reset,
}: {
- params: Promise<{ locale: string }>;
error: Error & { digest?: string };
reset: () => void;
}) {
+
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
- const { locale } = use(params);
- setRequestLocale(locale);
- const t = useTranslations("error");
+ const t = useTranslations("ErrorPage");
return (
@@ -34,12 +30,15 @@ export default function Error({
{t("title")}
{t("sorry")}
+ {/*
+ {`${error.cause}`}
+
*/}
diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx
index 89959c5..7549258 100644
--- a/src/app/[locale]/page.tsx
+++ b/src/app/[locale]/page.tsx
@@ -1,5 +1,4 @@
-import { use } from "react";
-import { getTranslations, setRequestLocale } from "next-intl/server";
+import { getTranslations } from "next-intl/server";
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import Editor from "@/components/SimpleEditor";
@@ -42,7 +41,7 @@ export default function HomePage() {
{t("toolTitle")}
-
+
diff --git a/src/components/FullEditor.tsx b/src/components/FullEditor.tsx
index e345fd2..9f4da08 100644
--- a/src/components/FullEditor.tsx
+++ b/src/components/FullEditor.tsx
@@ -7,6 +7,8 @@ import PreviewToolbar from "./common/PreviewToolbar";
import { useState } from "react";
import { useTranslations } from "next-intl";
import TextSetting, { TextProp } from "./common/TextSetting";
+import { useSearchParams } from "next/navigation";
+import { decodeText } from "@/lib/utils";
/**
* 全特性工具栏
@@ -16,12 +18,16 @@ export default function Page({ textProp, backgroundProp }: { textProp: TextProp
const t = useTranslations("TextEditor");
- const [background, setBackground] = useState({
+ backgroundProp = backgroundProp || {
type: "color",
color: "#c4b1b1",
image: null,
- });
- const [text, setText] = useState(TextProp.default(t("defaultText")));
+ } satisfies BackgroundProp;
+
+ textProp = textProp || TextProp.default(t("defaultText"));
+
+ const [background, setBackground] = useState(backgroundProp!);
+ const [text, setText] = useState(textProp!);
return (
diff --git a/src/components/SimpleEditor.tsx b/src/components/SimpleEditor.tsx
index b7c143e..c1daa78 100644
--- a/src/components/SimpleEditor.tsx
+++ b/src/components/SimpleEditor.tsx
@@ -14,39 +14,43 @@ import { useRouter } from "@/i18n/navigation";
* 简易工具
* @returns
*/
-export default function Page({ textProp, backgroundProp }: { textProp: TextProp | undefined, backgroundProp: BackgroundProp | undefined }) {
+export default function Page() {
const t = useTranslations("TextEditor");
const tIndex = useTranslations("HomePage");
const router = useRouter();
- const [background, setBackground] = useState(backgroundProp || {
+ let backgroundProp = {
type: "color",
color: "#c4b1b1",
image: null,
- });
+ } satisfies BackgroundProp;
- const [text, setText] = useState(textProp || TextProp.default(t("defaultText")));
+ let textProp = TextProp.default(t("defaultText"));
- useEffect(() => {
- let bg = sessionStorage.getItem("background");
+ const [background, setBackground] = useState(backgroundProp);
- if (bg) {
- console.log("初始化设置 bg", bg);
+ const [text, setText] = useState(textProp);
- setBackground(JSON.parse(bg));
- }
+ // useEffect(() => {
+ // let bg = sessionStorage.getItem("background");
- let txt = sessionStorage.getItem("text");
+ // if (bg) {
+ // console.log("初始化设置 bg", bg);
- if (txt) {
- console.log("初始化设置 txt", txt);
+ // setBackground(JSON.parse(bg));
+ // }
- setText(JSON.parse(txt));
- }
+ // let txt = sessionStorage.getItem("text");
- }, []);
+ // if (txt) {
+ // console.log("初始化设置 txt", txt);
+
+ // setText(JSON.parse(txt));
+ // }
+
+ // }, []);
useEffect(() => {
sessionStorage.setItem("background", JSON.stringify(background));
@@ -65,12 +69,11 @@ export default function Page({ textProp, backgroundProp }: { textProp: TextProp
setBackground={setBackground}
/>
+ { router.push("/editor") }}>{tIndex("toolMore")} ?
-
- { router.push("/editor") }}>{tIndex("toolMore")}?
);
diff --git a/src/components/common/PreviewToolbar.tsx b/src/components/common/PreviewToolbar.tsx
index cd94c6a..f7a61a3 100644
--- a/src/components/common/PreviewToolbar.tsx
+++ b/src/components/common/PreviewToolbar.tsx
@@ -1,11 +1,12 @@
"use client";
import { useState, useRef, useEffect, } from "react";
-import { useTranslations } from "next-intl";
-import { Eye, Download } from "lucide-react";
+import { useLocale, useTranslations } from "next-intl";
+import { Eye, Download, Share } from "lucide-react";
import { BackgroundProp } from "./BackgroundSelector";
-import { Text, Flex } from "@radix-ui/themes";
+import { Text, Flex, Button, Select, AlertDialog, Code, Blockquote, Box } from "@radix-ui/themes";
import { getPicture, resize, init as threeInit, updateBackground, updateTextProps } from "./ThreeTools";
import { TextProp } from "./TextSetting";
+import { encodeText, getShareLink } from "@/lib/utils";
const Sizes = [
"1920x1080",
@@ -35,6 +36,9 @@ export default function PreviewToolbar({
const container = useRef(null);
const fullscreenElement = useRef(null);
const [picture, setPicture] = useState(null);
+ const [shareError, setShareError] = useState(null);
+ const [shareLink, setShareLink] = useState(null);
+ const locale = useLocale();
const updateSize = () => {
const split = Sizes[aspectRadio].split("x").map(Number);
@@ -76,7 +80,7 @@ export default function PreviewToolbar({
updateTextProps(text);
console.log("text change", text);
- }, 1000);
+ }, 200);
return () => clearTimeout(timeoutId);
}, [text]);
@@ -124,6 +128,35 @@ export default function PreviewToolbar({
};
+ const handleShare = () => {
+ setShareError(null);
+ setShareLink(null);
+ if (background.type == "image" && background.image) {
+ setShareError(t("shareErrorNotSupportDesc"));
+ return;
+ }
+
+ const bg = { ...background, image: null };
+ let txt = JSON.stringify({ bg, text });
+ txt = encodeText(txt);
+
+
+ const link = getShareLink(txt, locale);
+
+ setShareLink(link);
+
+ }
+
+ const copyLink = () => {
+ if (shareLink) {
+ navigator.clipboard.writeText(shareLink).catch(err => {
+ console.error("copy error:", err);
+ alert("copy error");
+ });
+ }
+
+ }
+
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "F11") {
@@ -160,33 +193,76 @@ export default function PreviewToolbar({
)}
-
+
+
+ {/* 分享按钮 */}
+
+
+
+
+
+ {t("share")}
+ {!shareError ? (
+
+ {t("shareSuccessDesc")} !
+
+
+ {shareLink}
+
+ ) :
+ (
+ {shareError}
+ )}
+
+
+ {!shareError &&
+
+
+ }
+
+
+
+
+
+
+
+
+
+
-
+ setAspectRadio(parseInt(e))}>
+
+
+ {AspectRatio.map((_, i) => {Sizes[i]})}
+
+
-
+
diff --git a/src/components/common/SimpleTextSetting.tsx b/src/components/common/SimpleTextSetting.tsx
index 11a64f8..523379b 100644
--- a/src/components/common/SimpleTextSetting.tsx
+++ b/src/components/common/SimpleTextSetting.tsx
@@ -1,4 +1,4 @@
-import { Flex, Heading } from "@radix-ui/themes";
+import { Flex, Heading, Select } from "@radix-ui/themes";
import { useTranslations } from "next-intl";
import { FontNames, FontWeights, TextProp } from "./TextSetting";
@@ -12,7 +12,7 @@ export default function TextSetting({
const t = useTranslations("TextEditor");
return (
-
+
{t("title")}
-
+
@@ -31,37 +31,29 @@ export default function TextSetting({
className="w-full h-10 rounded-md cursor-pointer"
/>
-
+
-
+ setText({ ...text, font: e })}>
+
+
+ {FontNames.map((name) => {name})}
+
+
-
+
-
+
+ setText({ ...text, weight: e })}>
+
+
+ {FontWeights.map((name) => {name})}
+
+
+
);
diff --git a/src/components/common/TextSetting.tsx b/src/components/common/TextSetting.tsx
index 71490ee..5a2cd37 100644
--- a/src/components/common/TextSetting.tsx
+++ b/src/components/common/TextSetting.tsx
@@ -1,4 +1,4 @@
-import { Flex, Heading } from "@radix-ui/themes";
+import { Flex, Heading, Select } from "@radix-ui/themes";
import { useTranslations } from "next-intl";
export const FontWeights = ["regular", "bold"];
@@ -92,33 +92,25 @@ export default function TextSetting({
-
+
setText({ ...text, font: e })}>
+
+
+ {FontNames.map((name) => {name})}
+
+
-
+
+ setText({ ...text, weight: e })}>
+
+
+ {FontWeights.map((name) => {name})}
+
+
+
);
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index bd0c391..b9eccc1 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,27 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
-
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+import CryptoJS from "crypto-js";
+import LZString from "lz-string";
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
+}
+
+const SECRET_KEY = "fast3dtext-ymk";
+export function encodeText(text: string) {
+ const compressed = LZString.compressToEncodedURIComponent(text);
+ const encrypted = CryptoJS.AES.encrypt(compressed, SECRET_KEY).toString();
+ return encodeURIComponent(encrypted);
+}
+
+export function decodeText(encodedText: string) {
+ const decoded = decodeURIComponent(encodedText);
+ const decrypted = CryptoJS.AES.decrypt(decoded, SECRET_KEY).toString(
+ CryptoJS.enc.Utf8
+ );
+ const decompressed = LZString.decompressFromEncodedURIComponent(decrypted);
+ return decompressed;
+}
+
+export function getShareLink(data: string, locale: string) {
+ return `${window.location.origin}/${locale}/editor/${data}`;
}