diff --git a/src/app/(main)/i/[subdomain]/edit/page.tsx b/src/app/(main)/i/[subdomain]/edit/page.tsx index 52cbda4..760b2c1 100644 --- a/src/app/(main)/i/[subdomain]/edit/page.tsx +++ b/src/app/(main)/i/[subdomain]/edit/page.tsx @@ -1,3 +1,4 @@ +import { notFound } from "next/navigation"; import Editor from "~/components/editor"; import { getInvitationByEventUrl } from "~/lib/db/schema/invitations.query"; @@ -8,6 +9,10 @@ export default async function Page({ }) { const invitation = await getInvitationByEventUrl(params.subdomain); + if (!invitation) { + notFound(); + } + return ( { + const invitation = await getInvitationByEventUrl(params.subdomain); + + if (!invitation) { + return {}; + } + const previousImages = (await parent).openGraph?.images || []; + + return { + title: { + default: invitation.title, + template: "%s | 인비", + }, + }; +} + +export default async function Page({ params }: Props) { + const invitation = await getInvitationByEventUrl(params.subdomain); + + if (!invitation) { + notFound(); + } + + return ( + +
+ {Array.isArray(invitation.customFields) && + invitation.customFields.map((childElement) => ( + + ))} +
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index bdbaf66..513a8df 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,8 +4,8 @@ import { pretendard } from "~/lib/fonts"; import "./globals.css"; export const metadata: Metadata = { - title: "인비", - description: "당신의 환대, 초대장 플랫폼 '인비' 입니다.", + title: "초대장 플랫폼, 인비", + description: "따뜻한 마음을 담아 당신의 환대를 전해보세요.", }; export default function RootLayout({ diff --git a/src/components/editor/elements/image-element.tsx b/src/components/editor/elements/image-element.tsx index 733bee6..6e0aaf5 100644 --- a/src/components/editor/elements/image-element.tsx +++ b/src/components/editor/elements/image-element.tsx @@ -13,7 +13,8 @@ export default function ImageElement({ element }: Props) { {element.content.src ? ( {element.content.alt diff --git a/src/components/editor/elements/navigation-element.tsx b/src/components/editor/elements/navigation-element.tsx index 32a87cb..af63cbd 100644 --- a/src/components/editor/elements/navigation-element.tsx +++ b/src/components/editor/elements/navigation-element.tsx @@ -46,13 +46,31 @@ export default function NavigationElement({ element }: Props) { return ( ); diff --git a/src/components/editor/navigation.tsx b/src/components/editor/navigation.tsx index b883ea9..094756a 100644 --- a/src/components/editor/navigation.tsx +++ b/src/components/editor/navigation.tsx @@ -6,8 +6,8 @@ import { EllipsisVerticalIcon, EyeIcon, Laptop, + MailOpenIcon, Redo2, - Share2Icon, Smartphone, Tablet, Undo2, @@ -145,8 +145,10 @@ export default function EditorNavigation() { - diff --git a/src/components/editor/provider.tsx b/src/components/editor/provider.tsx index ee8da4e..95c1cdb 100644 --- a/src/components/editor/provider.tsx +++ b/src/components/editor/provider.tsx @@ -23,6 +23,7 @@ export const EditorContext = createContext<{ export type EditorProps = { editorData?: EditorData; editorConfig?: Partial; + editorState?: Partial; }; export type EditorProviderProps = EditorProps & { @@ -33,6 +34,7 @@ export default function EditorProvider({ children, editorConfig, editorData, + editorState, }: EditorProviderProps) { const [editor, dispatch] = useReducer(editorReducer, { ...initialEditor, @@ -41,6 +43,10 @@ export default function EditorProvider({ ...initialEditorConfig, ...editorConfig, }, + state: { + ...initialEditor.state, + ...editorState, + }, }); return ( diff --git a/src/components/editor/sidebar-element-settings-tab/index.tsx b/src/components/editor/sidebar-element-settings-tab/index.tsx index 1f81322..589dc30 100644 --- a/src/components/editor/sidebar-element-settings-tab/index.tsx +++ b/src/components/editor/sidebar-element-settings-tab/index.tsx @@ -38,6 +38,7 @@ export default function SidebarElementSettingsTab(props: Props) { {selectedElement.type === "text" && ( <> + )} @@ -53,6 +54,7 @@ export default function SidebarElementSettingsTab(props: Props) { selectedElement.type === "2Col") && ( <> + diff --git a/src/components/editor/sidebar-settings-tab.tsx b/src/components/editor/sidebar-settings-tab.tsx index e879027..61a55c8 100644 --- a/src/components/editor/sidebar-settings-tab.tsx +++ b/src/components/editor/sidebar-settings-tab.tsx @@ -1,5 +1,6 @@ "use client"; +import { useForm } from "@tanstack/react-form"; import { useMutation } from "@tanstack/react-query"; import { LinkIcon } from "lucide-react"; import { toast } from "sonner"; @@ -13,6 +14,10 @@ import { SheetHeader, SheetTitle, } from "~/components/ui/sheet"; +import { + existsByEventUrl, + updateInvitation, +} from "~/lib/db/schema/invitations.query"; import { uploadImage } from "~/lib/image"; type Props = {}; @@ -34,36 +39,105 @@ export default function SidebarSettingsTab(props: Props) { } function CustomDomainSection() { - const { editor } = useEditor(); + const { editor, dispatch } = useEditor(); + + const updateSubdomainMutation = useMutation({ + mutationFn: async (subdomain: string) => { + const isExist = await existsByEventUrl(subdomain); + + if (isExist) { + throw new Error("이미 사용중인 도메인입니다."); + } + + await updateInvitation({ + id: editor.config.invitationId, + eventUrl: subdomain, + }); + + dispatch({ + type: "UPDATE_CONFIG", + payload: { + invitationSubdomain: subdomain, + }, + }); + + return subdomain; + }, + onSuccess: (subdomain) => { + toast.success("도메인이 변경되었습니다.", { + description: `https://${subdomain}.invi.my`, + }); + }, + onError: () => { + toast.error("도메인 변경에 실패했습니다."); + }, + }); + + const form = useForm({ + defaultValues: { + subdomain: editor.config.invitationSubdomain, + }, + onSubmit: async ({ value }) => { + if (value.subdomain === editor.config.invitationSubdomain) { + return; + } + + await updateSubdomainMutation.mutateAsync(value.subdomain); + }, + }); return (

도메인 설정

-
- - .invi.my - -
- } - defaultValue={editor.config.invitationSubdomain} - onDebounceChange={() => {}} +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + > + { + return ( + field.handleChange(e.target.value)} + className="pr-0.5 ring-1" + componentSuffix={ +
+ .invi.my + +
+ } + /> + ); + }} />
- {true && ( + {updateSubdomainMutation.isError && ( - 중복된 도메인입니다. 다른 주소를 사용해주세요. + {updateSubdomainMutation.error?.message} )}
-
+ ); } diff --git a/src/middleware.ts b/src/middleware.ts index 6524489..bc03fa7 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -5,30 +5,40 @@ export const config = { matcher: ["/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)"], }; +function isSubdomain(hostname: string) { + const subdomain = hostname.split(".")[0]; + + if (!subdomain) { + return false; + } + + return ( + !hostname.startsWith("localhost:") && !["www", "invi"].includes(hostname) + ); +} + export function middleware(request: NextRequest) { const url = request.nextUrl; const hostname = request.headers.get("host")!; const searchParams = url.searchParams.toString(); const path = `${url.pathname}${searchParams ? `?${searchParams}` : ""}`; - const isSubdomain = (str: string) => { - return !str.startsWith("localhost:") && !["www", "invi"].includes(str); - }; - - const hostnameParts = hostname.split("."); - const potentialSubdomain = hostnameParts[0]; - - let subdomain: string | null = null; - - if (isSubdomain(potentialSubdomain)) { - subdomain = potentialSubdomain; + // 서브도메인으로 접근한 경우 + if (isSubdomain(hostname)) { + const subdomain = hostname.split(".")[0]; + return NextResponse.rewrite(new URL(`/i/${subdomain}${path}`, request.url)); } - if (subdomain) { - return NextResponse.rewrite( - new URL(`/pg/${subdomain}${path}`, request.url), - ); - } else { - return NextResponse.rewrite(new URL(path, request.url)); + // /i/* 경로로 접근한 경우 + const match = path.match(/^\/i\/([^\/]+)(\/.*)?$/); + if (match) { + const subdomain = match[1]; + const remainingPath = match[2] || ""; + const newUrl = new URL(request.url); + newUrl.hostname = `${subdomain}.${newUrl.hostname}`; + newUrl.pathname = remainingPath; + return NextResponse.redirect(newUrl); } + + return NextResponse.rewrite(new URL(path, request.url)); }