release 0.3.4
This commit is contained in:
116
apps/web/ui/layout/active-licence-modal.tsx
Normal file
116
apps/web/ui/layout/active-licence-modal.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import Modal from "@/ui/shared/modal";
|
||||
import { signIn } from "next-auth/react";
|
||||
import {
|
||||
useState,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { LoadingDots, Google, Github } from "@/ui/shared/icons";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
import { Session } from "next-auth";
|
||||
import { useUserInfoByEmail } from "@/app/post/[id]/request";
|
||||
|
||||
const EditNicknameModal = ({
|
||||
session,
|
||||
showEditModal,
|
||||
setShowEditModal,
|
||||
}: {
|
||||
session: Session | null;
|
||||
showEditModal: boolean;
|
||||
setShowEditModal: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const { user } = useUserInfoByEmail(session?.user?.email || "");
|
||||
const [nickname, setNickname] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isSendSuccess, setIsSendSuccess] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!session?.user || !user || !nickname) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetcher("/api/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
userId: user.id,
|
||||
userName: nickname,
|
||||
}),
|
||||
});
|
||||
if (res) {
|
||||
setLoading(false);
|
||||
setIsSendSuccess(true);
|
||||
setShowEditModal(false);
|
||||
}
|
||||
};
|
||||
const handleKeydown = (key: string) => {
|
||||
if (key === "Enter") {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal showModal={showEditModal} setShowModal={setShowEditModal}>
|
||||
<div className="w-full overflow-hidden bg-gray-50 shadow-xl md:max-w-md md:rounded-2xl md:border md:border-gray-200">
|
||||
<div className="flex flex-col items-center justify-center space-y-3 border-b border-gray-200 bg-white px-4 py-6 pt-8 text-center md:px-16">
|
||||
<h3 className="font-display text-2xl font-bold">Your Name</h3>
|
||||
{/* <p className="text-sm text-gray-500"></p> */}
|
||||
</div>
|
||||
|
||||
<div className="px-14 py-10">
|
||||
<input
|
||||
className="shadow-blue-gray-200 mb-4 w-full rounded-md border border-slate-200 bg-[#f8f8f8a1] px-3 py-3 text-sm placeholder-gray-400 shadow-inner"
|
||||
type="text"
|
||||
// value={user?.name}
|
||||
placeholder={user?.name || "Enter your"}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
onKeyDown={(e) => handleKeydown(e.key)}
|
||||
/>
|
||||
<button
|
||||
// disabled={loading}
|
||||
onClick={handleSubmit}
|
||||
className={`
|
||||
${
|
||||
loading
|
||||
? "border-gray-300 bg-gray-200"
|
||||
: `${
|
||||
isSendSuccess
|
||||
? " border-blue-500 bg-blue-500 hover:text-blue-500"
|
||||
: "border-black bg-black hover:text-black"
|
||||
} hover:bg-gray-100`
|
||||
}
|
||||
h-10 w-full rounded-md border px-2 py-1 text-sm text-slate-100 transition-all `}
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingDots color="gray" />
|
||||
) : (
|
||||
<div className="flex items-center justify-center">
|
||||
{isSendSuccess && !loading ? "修改成功" : "提交"}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export function useEditNicknameModal(session: Session | null) {
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
|
||||
const EditModalCallback = useCallback(() => {
|
||||
return (
|
||||
<EditNicknameModal
|
||||
session={session}
|
||||
showEditModal={showEditModal}
|
||||
setShowEditModal={setShowEditModal}
|
||||
/>
|
||||
);
|
||||
}, [showEditModal, setShowEditModal]);
|
||||
|
||||
return useMemo(
|
||||
() => ({ setShowEditModal, EditModal: EditModalCallback }),
|
||||
[setShowEditModal, EditModalCallback],
|
||||
);
|
||||
}
|
196
apps/web/ui/layout/create-room-modal.tsx
Normal file
196
apps/web/ui/layout/create-room-modal.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import Modal from "@/ui/shared/modal";
|
||||
import {
|
||||
useState,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { LoadingDots } from "@/ui/shared/icons";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
import { Session } from "next-auth";
|
||||
import { useUserInfoByEmail } from "@/app/post/[id]/request";
|
||||
import shortid from "shortid";
|
||||
import { IResponse } from "@/lib/types/response";
|
||||
import { Collaboration } from "@prisma/client";
|
||||
import toast from "react-hot-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { defaultEditorContent } from "@/lib/consts";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { ContentItem } from "@/lib/types/note";
|
||||
import { Shapes } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { addNote } from "@/store/db.model";
|
||||
|
||||
const CreatRoomModal = ({
|
||||
initTitle,
|
||||
localId,
|
||||
session,
|
||||
showEditModal,
|
||||
setShowEditModal,
|
||||
}: {
|
||||
initTitle: string;
|
||||
localId: string;
|
||||
session: Session | null;
|
||||
showEditModal: boolean;
|
||||
setShowEditModal: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const [title, setTitle] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isSendSuccess, setIsSendSuccess] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!session?.user) {
|
||||
toast("Please login first");
|
||||
return;
|
||||
}
|
||||
if (!title || title.length < 3 || title.length > 20) {
|
||||
toast("Invalid space name");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const new_localId = uuidv4();
|
||||
|
||||
const roomId = shortid.generate().replace("_", "A").replace("-", "a");
|
||||
const res = await fetcher<IResponse<Collaboration | null>>(
|
||||
"/api/collaboration",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
roomId,
|
||||
localId: new_localId, // 本地创建新笔记关联空间
|
||||
title,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (res.code !== 200) {
|
||||
toast(res.msg, {
|
||||
icon: "😅",
|
||||
});
|
||||
} else {
|
||||
const date = new Date();
|
||||
const newItem: ContentItem = {
|
||||
id: new_localId,
|
||||
title: `Untitled-${new_localId.slice(0, 6)}-${
|
||||
date.getMonth() + 1
|
||||
}/${date.getDate()}`,
|
||||
content: defaultEditorContent,
|
||||
tag: "",
|
||||
created_at: date.getTime(),
|
||||
updated_at: date.getTime(),
|
||||
};
|
||||
addNote(newItem);
|
||||
toast.success(res.msg, {
|
||||
icon: "🎉",
|
||||
});
|
||||
router.push(`/post/${new_localId}?work=${roomId}`);
|
||||
}
|
||||
if (res) {
|
||||
setLoading(false);
|
||||
setIsSendSuccess(true);
|
||||
setShowEditModal(false);
|
||||
}
|
||||
};
|
||||
const handleKeydown = (key: string) => {
|
||||
if (key === "Enter") {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal showModal={showEditModal} setShowModal={setShowEditModal}>
|
||||
<div className="w-full overflow-hidden bg-gray-50 shadow-xl md:max-w-md md:rounded-2xl md:border md:border-gray-200">
|
||||
<div className="flex flex-col items-center justify-center space-y-3 border-b border-gray-200 bg-white px-4 py-6 pt-8 text-center md:px-10">
|
||||
<Shapes className="inline h-12 w-12 text-cyan-500" />
|
||||
<h3 className="font-display text-2xl font-bold">
|
||||
Collaboration space
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="px-14 py-10">
|
||||
<input
|
||||
className="shadow-blue-gray-200 mb-4 w-full rounded-md border border-slate-200 bg-[#f8f8f8a1] px-3 py-3 text-sm placeholder-gray-400 shadow-inner"
|
||||
type="text"
|
||||
placeholder={"Enter space name (3-20 characters)"}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
onKeyDown={(e) => handleKeydown(e.key)}
|
||||
/>
|
||||
<button
|
||||
// disabled={loading}
|
||||
onClick={handleSubmit}
|
||||
className={`
|
||||
${
|
||||
loading
|
||||
? "border-gray-300 bg-gray-200"
|
||||
: `${
|
||||
isSendSuccess
|
||||
? " border-blue-500 bg-blue-500 hover:text-blue-500"
|
||||
: "border-black bg-black hover:text-black"
|
||||
} hover:bg-gray-100`
|
||||
}
|
||||
h-10 w-full rounded-md border px-2 py-1 text-sm text-slate-100 transition-all `}
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingDots color="gray" />
|
||||
) : (
|
||||
<div className="flex items-center justify-center">
|
||||
{isSendSuccess && !loading ? "Success!" : "Create"}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<p className="mt-6 text-start text-sm text-gray-600">
|
||||
You are creating a collaboration space, just enter the space name
|
||||
and click <strong className="text-cyan-500">Create</strong>.
|
||||
</p>
|
||||
<p className="text-start text-sm text-gray-600">
|
||||
Once created successfully, it will automatically jump to your
|
||||
collaboration space and generate an{" "}
|
||||
<strong className="text-cyan-500">invitation link</strong>, which
|
||||
allows you to invite your team to join the collaboration.
|
||||
</p>
|
||||
<p className="text-start text-sm text-gray-600">
|
||||
{" "}
|
||||
See more about{" "}
|
||||
<Link
|
||||
className="text-cyan-500 after:content-['_↗'] hover:opacity-80"
|
||||
href={`/collaboration`}
|
||||
target="_blank"
|
||||
>
|
||||
collaboration
|
||||
</Link>{" "}
|
||||
space.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export function useCreatRoomModal(
|
||||
session: Session | null,
|
||||
initTitle: string,
|
||||
localId: string,
|
||||
) {
|
||||
const [showRoomModal, setShowRoomModal] = useState(false);
|
||||
|
||||
const RoomModalCallback = useCallback(() => {
|
||||
return (
|
||||
<CreatRoomModal
|
||||
initTitle={initTitle}
|
||||
localId={localId}
|
||||
session={session}
|
||||
showEditModal={showRoomModal}
|
||||
setShowEditModal={setShowRoomModal}
|
||||
/>
|
||||
);
|
||||
}, [showRoomModal, setShowRoomModal]);
|
||||
|
||||
return useMemo(
|
||||
() => ({ setShowRoomModal, RoomModal: RoomModalCallback }),
|
||||
[setShowRoomModal, RoomModalCallback],
|
||||
);
|
||||
}
|
115
apps/web/ui/layout/edit-nickname-modal.tsx
Normal file
115
apps/web/ui/layout/edit-nickname-modal.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import Modal from "@/ui/shared/modal";
|
||||
import {
|
||||
useState,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { LoadingDots } from "@/ui/shared/icons";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
import { Session } from "next-auth";
|
||||
import { useUserInfoByEmail } from "@/app/post/[id]/request";
|
||||
|
||||
const EditNicknameModal = ({
|
||||
session,
|
||||
showEditModal,
|
||||
setShowEditModal,
|
||||
}: {
|
||||
session: Session | null;
|
||||
showEditModal: boolean;
|
||||
setShowEditModal: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const { user } = useUserInfoByEmail(session?.user?.email || "");
|
||||
const [nickname, setNickname] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isSendSuccess, setIsSendSuccess] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!session?.user || !user || !nickname) return;
|
||||
if (nickname.length < 3 || nickname.length > 20) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetcher("/api/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
userId: user.id,
|
||||
userName: nickname,
|
||||
}),
|
||||
});
|
||||
if (res) {
|
||||
setLoading(false);
|
||||
setIsSendSuccess(true);
|
||||
setShowEditModal(false);
|
||||
}
|
||||
};
|
||||
const handleKeydown = (key: string) => {
|
||||
if (key === "Enter") {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal showModal={showEditModal} setShowModal={setShowEditModal}>
|
||||
<div className="w-full overflow-hidden bg-gray-50 shadow-xl md:max-w-md md:rounded-2xl md:border md:border-gray-200">
|
||||
<div className="flex flex-col items-center justify-center space-y-3 border-b border-gray-200 bg-white px-4 py-6 pt-8 text-center md:px-16">
|
||||
<h3 className="font-display text-2xl font-bold">Edit nickname</h3>
|
||||
{/* <p className="text-sm text-gray-500"></p> */}
|
||||
</div>
|
||||
|
||||
<div className="px-14 py-10">
|
||||
<input
|
||||
className="shadow-blue-gray-200 mb-4 w-full rounded-md border border-slate-200 bg-[#f8f8f8a1] px-3 py-3 text-sm placeholder-gray-400 shadow-inner"
|
||||
type="text"
|
||||
placeholder={user?.name || "Enter nickname (3-20 characters)"}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
onKeyDown={(e) => handleKeydown(e.key)}
|
||||
/>
|
||||
<button
|
||||
// disabled={loading}
|
||||
onClick={handleSubmit}
|
||||
className={`
|
||||
${
|
||||
loading
|
||||
? "border-gray-300 bg-gray-200"
|
||||
: `${
|
||||
isSendSuccess
|
||||
? " border-blue-500 bg-blue-500 hover:text-blue-500"
|
||||
: "border-black bg-black hover:text-black"
|
||||
} hover:bg-gray-100`
|
||||
}
|
||||
h-10 w-full rounded-md border px-2 py-1 text-sm text-slate-100 transition-all `}
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingDots color="gray" />
|
||||
) : (
|
||||
<div className="flex items-center justify-center">
|
||||
{isSendSuccess && !loading ? "Success!" : "Submit"}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export function useEditNicknameModal(session: Session | null) {
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
|
||||
const EditModalCallback = useCallback(() => {
|
||||
return (
|
||||
<EditNicknameModal
|
||||
session={session}
|
||||
showEditModal={showEditModal}
|
||||
setShowEditModal={setShowEditModal}
|
||||
/>
|
||||
);
|
||||
}, [showEditModal, setShowEditModal]);
|
||||
|
||||
return useMemo(
|
||||
() => ({ setShowEditModal, EditModal: EditModalCallback }),
|
||||
[setShowEditModal, EditModalCallback],
|
||||
);
|
||||
}
|
146
apps/web/ui/layout/email-login-button.tsx
Normal file
146
apps/web/ui/layout/email-login-button.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { signIn } from "next-auth/react";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import { isEmail } from "@/lib/utils";
|
||||
import { Github, Google, LoadingDots } from "../shared/icons";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export default function EmailButton() {
|
||||
const pathname = usePathname();
|
||||
const [email, setEmail] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isSendSuccess, setIsSendSuccess] = useState(false);
|
||||
const [signInGithubClicked, setSignInGithubClicked] = useState(false);
|
||||
const [signInGoogleClicked, setSignInGoogleClicked] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (email === "") {
|
||||
toast("Empty email", {
|
||||
icon: "😅",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!isEmail(email)) {
|
||||
toast("Invalid email format", {
|
||||
icon: "😅",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSendSuccess(false);
|
||||
setLoading(true);
|
||||
|
||||
const sign_req = await signIn("email", {
|
||||
email: email,
|
||||
callbackUrl: pathname,
|
||||
redirect: false,
|
||||
});
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 20000);
|
||||
if (sign_req?.ok) {
|
||||
setLoading(false);
|
||||
setIsSendSuccess(true);
|
||||
} else if (sign_req?.error) {
|
||||
toast("Sending failed, please try again", {
|
||||
icon: "😅",
|
||||
});
|
||||
setLoading(false);
|
||||
setIsSendSuccess(false);
|
||||
}
|
||||
};
|
||||
const handleKeydown = (key: string) => {
|
||||
if (key === "Enter") {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster />
|
||||
<input
|
||||
className="mb-4 rounded-md border border-slate-200 px-3 py-3 shadow-inner"
|
||||
type="text"
|
||||
placeholder="Enter your email"
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onKeyDown={(e) => handleKeydown(e.key)}
|
||||
/>
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={handleSubmit}
|
||||
className={`
|
||||
${
|
||||
loading
|
||||
? "border-gray-300 bg-gray-200"
|
||||
: ` ${
|
||||
isSendSuccess
|
||||
? "border-blue-500 bg-blue-500 hover:text-blue-500"
|
||||
: "border-black bg-black hover:text-black"
|
||||
} hover:bg-gray-100`
|
||||
}
|
||||
h-10 w-full rounded-md border px-2 py-1 text-sm text-slate-100 transition-all `}
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingDots color="gray" />
|
||||
) : (
|
||||
<span className="font-medium">
|
||||
{isSendSuccess && !loading
|
||||
? "Successfully sent! please check email"
|
||||
: "Sign in with email"}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="my-3 flex items-center justify-center">
|
||||
<span className="w-full border border-b-0"></span>
|
||||
<span className="px-3 text-gray-400">or</span>
|
||||
<span className="w-full border border-b-0"></span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
disabled={signInGithubClicked}
|
||||
className={`${
|
||||
signInGithubClicked
|
||||
? "cursor-not-allowed bg-gray-100"
|
||||
: "border text-black hover:bg-gray-50"
|
||||
} nice-border flex h-10 w-full items-center justify-center space-x-3 rounded-md border text-sm shadow transition-all duration-75 hover:border-gray-800 focus:outline-none`}
|
||||
onClick={() => {
|
||||
setSignInGithubClicked(true);
|
||||
signIn("github", { callbackUrl: pathname, redirect: false });
|
||||
}}
|
||||
>
|
||||
{signInGithubClicked ? (
|
||||
<LoadingDots color="#808080" />
|
||||
) : (
|
||||
<>
|
||||
<Github className="h-5 w-5" />
|
||||
<p>Sign in with Github</p>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
disabled={signInGoogleClicked}
|
||||
className={`${
|
||||
signInGoogleClicked
|
||||
? "cursor-not-allowed bg-gray-100"
|
||||
: "border text-black hover:bg-gray-50"
|
||||
} nice-border flex h-10 w-full items-center justify-center space-x-3 rounded-md border text-sm shadow transition-all duration-75 hover:border-gray-800 focus:outline-none`}
|
||||
onClick={() => {
|
||||
setSignInGoogleClicked(true);
|
||||
signIn("google", { callbackUrl: pathname, redirect: false });
|
||||
}}
|
||||
>
|
||||
{signInGoogleClicked ? (
|
||||
<LoadingDots color="#808080" />
|
||||
) : (
|
||||
<>
|
||||
<Google className="h-5 w-5" />
|
||||
<p>Sign in with Google</p>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
27
apps/web/ui/layout/footer-publish.tsx
Normal file
27
apps/web/ui/layout/footer-publish.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Logo from "../shared/icons/logo";
|
||||
|
||||
export default function FooterPublish() {
|
||||
return (
|
||||
<div className=" h-32 w-full bg-white py-5 text-center ">
|
||||
<Link
|
||||
href="/"
|
||||
className="font-display flex flex-col items-center justify-center text-xl"
|
||||
>
|
||||
<Logo className="h-8 w-8" />
|
||||
</Link>
|
||||
|
||||
<div className="mt-2 flex items-center justify-center gap-1 text-sm">
|
||||
<span>Powered by</span>
|
||||
<Link href="/" className="font-bold">
|
||||
<span className="bg-gradient-to-r from-cyan-500 via-cyan-600 to-cyan-800 bg-clip-text text-transparent ">
|
||||
INKE & AI Notebook
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
{/* <p>Thanks A Note</p> */}
|
||||
</div>
|
||||
);
|
||||
}
|
114
apps/web/ui/layout/footer.tsx
Normal file
114
apps/web/ui/layout/footer.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Github, Mail, MessageSquare, Youtube } from "lucide-react";
|
||||
import ProductHunt from "../shared/icons/product-hunt";
|
||||
import pkg from "../../package.json";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<div className="mt-6 flex h-full min-h-[256px] w-screen flex-col items-start justify-between gap-4 bg-slate-800 px-5 py-8 sm:flex-row sm:px-16">
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<Link href="/" className="mb-4 flex items-end text-3xl font-bold">
|
||||
<span className="bg-gradient-to-r from-cyan-500 via-cyan-600 to-cyan-800 bg-clip-text text-transparent ">
|
||||
INKE
|
||||
</span>
|
||||
<span className="text-slate-300">.AI</span>
|
||||
</Link>
|
||||
<p className="flex items-center gap-1 font-mono text-slate-200">
|
||||
AI notebook, empowering writing.
|
||||
</p>
|
||||
|
||||
<div className="my-4 flex items-start justify-center gap-1 text-sm text-slate-400">
|
||||
<span className="text-slate-200">© 2023</span>
|
||||
<Link href="/" className="font-bold">
|
||||
<span className="bg-gradient-to-r from-slate-400 via-slate-500 to-slate-600 bg-clip-text text-transparent ">
|
||||
INK
|
||||
</span>
|
||||
<span className="text-slate-200">E</span>.
|
||||
</Link>
|
||||
All rights reserved. - Inke.app
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className="mb-6 mt-2"
|
||||
href="https://www.producthunt.com/products/inke/launches"
|
||||
target="_blank"
|
||||
>
|
||||
<ProductHunt />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-20 text-slate-200 sm:flex-row">
|
||||
<div className="flex flex-col items-start">
|
||||
<p className="mb-4 font-bold">Information</p>
|
||||
<Link
|
||||
className="mb-2 font-mono text-sm text-slate-200 hover:text-slate-400"
|
||||
href="/pricing"
|
||||
>
|
||||
Pricing
|
||||
</Link>
|
||||
<Link
|
||||
className="mb-2 font-mono text-sm text-slate-200 hover:text-slate-400"
|
||||
href="/document"
|
||||
>
|
||||
Document
|
||||
</Link>
|
||||
<Link
|
||||
className="mb-2 font-mono text-sm text-slate-200 hover:text-slate-400"
|
||||
href="/collaboration"
|
||||
>
|
||||
Collaboration
|
||||
</Link>
|
||||
<Link
|
||||
className="mb-2 font-mono text-sm text-slate-200 hover:text-slate-400"
|
||||
href="/shortcuts"
|
||||
>
|
||||
Shortcuts Reference
|
||||
</Link>
|
||||
<Link
|
||||
className="mb-2 font-mono text-sm text-slate-200 hover:text-slate-400"
|
||||
href="/privacy"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link
|
||||
className="flex items-center gap-1 font-mono text-sm text-slate-200 hover:text-slate-400"
|
||||
href="mailto:team@inke.app"
|
||||
>
|
||||
Contact Mail
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-start">
|
||||
<p className="mb-4 font-bold">Community</p>
|
||||
<Link
|
||||
className="mb-2 flex items-center gap-1 font-mono text-sm text-slate-200 hover:text-slate-400"
|
||||
href="/feedback"
|
||||
target="_blank"
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" /> Feedback
|
||||
</Link>
|
||||
<Link
|
||||
className="mb-2 flex items-center gap-1 font-mono text-sm text-slate-200 hover:text-slate-400"
|
||||
href="https://www.youtube.com/watch?v=Te3Piqtv1NQ"
|
||||
target="_blank"
|
||||
>
|
||||
<Youtube className="h-4 w-4" /> Youtube
|
||||
</Link>
|
||||
<Link
|
||||
className="mb-2 flex items-center justify-center gap-1 bg-gradient-to-r from-indigo-400 via-purple-400 to-purple-500 bg-clip-text text-sm font-semibold text-transparent hover:text-slate-400 "
|
||||
href="https://github.com/yesmore/inke"
|
||||
target="_blank"
|
||||
>
|
||||
<Github className="h-4 w-4 text-slate-100" />
|
||||
Open Source
|
||||
</Link>
|
||||
<span className="text-sm font-semibold text-slate-300">
|
||||
v{pkg.version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
8
apps/web/ui/layout/nav.tsx
Normal file
8
apps/web/ui/layout/nav.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import Navbar from "./navbar";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "app/api/auth/[...nextauth]/route";
|
||||
|
||||
export default async function Nav() {
|
||||
const session = await getServerSession(authOptions);
|
||||
return <Navbar session={session} />;
|
||||
}
|
91
apps/web/ui/layout/navbar.tsx
Normal file
91
apps/web/ui/layout/navbar.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import useScroll from "@/lib/hooks/use-scroll";
|
||||
import { useSignInModal } from "./sign-in-modal";
|
||||
import UserDropdown from "./user-dropdown";
|
||||
import { Session } from "next-auth";
|
||||
import { Suspense } from "react";
|
||||
import { useEditNicknameModal } from "./edit-nickname-modal";
|
||||
import Logo from "../shared/icons/logo";
|
||||
|
||||
export default function NavBar({ session }: { session: Session | null }) {
|
||||
const { SignInModal, setShowSignInModal } = useSignInModal();
|
||||
const { EditModal, setShowEditModal } = useEditNicknameModal(session);
|
||||
const scrolled = useScroll(50);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SignInModal />
|
||||
<EditModal />
|
||||
<div
|
||||
className={`fixed top-0 flex w-full justify-center ${
|
||||
scrolled
|
||||
? "bg-white/50 border-b border-gray-200 backdrop-blur-xl"
|
||||
: "bg-white/0"
|
||||
} z-[9999] transition-all`}
|
||||
>
|
||||
<div className="mx-5 flex h-16 w-full max-w-screen-xl items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Link
|
||||
href="/"
|
||||
className="font-display flex flex-col items-center justify-center text-xl"
|
||||
>
|
||||
<Logo className="h-5 w-5" />
|
||||
</Link>
|
||||
<Link href="/" className="font-bold">
|
||||
<span className="bg-gradient-to-r from-cyan-500 via-cyan-600 to-cyan-800 bg-clip-text text-transparent ">
|
||||
INKE
|
||||
</span>
|
||||
<span>.AI</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Suspense>
|
||||
<div className="flex items-center justify-center gap-3 md:gap-5">
|
||||
{/*<Link
|
||||
href="https://www.producthunt.com/products/inke?utm_source=badge-follow&utm_medium=badge&utm_souce=badge-inke"
|
||||
target="_blank"
|
||||
>
|
||||
<Image
|
||||
src="/follow.svg"
|
||||
alt="Inke | Product Hunt"
|
||||
width="86"
|
||||
height="32"
|
||||
/>
|
||||
</Link> */}
|
||||
|
||||
<Link
|
||||
className="text-slate-600 transition-all hover:text-slate-300"
|
||||
href={"/document"}
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<Link
|
||||
className="text-slate-600 transition-all hover:text-slate-300"
|
||||
href={"/pricing"}
|
||||
>
|
||||
Pricing
|
||||
</Link>
|
||||
|
||||
{session ? (
|
||||
<UserDropdown
|
||||
session={session}
|
||||
setShowEditModal={setShowEditModal}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className="bg-gradient-to-r from-slate-400 via-slate-600 to-slate-800 bg-clip-text py-1.5 font-semibold text-transparent transition-all hover:text-slate-300"
|
||||
onClick={() => setShowSignInModal(true)}
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
50
apps/web/ui/layout/not-found.tsx
Normal file
50
apps/web/ui/layout/not-found.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function UINotFound() {
|
||||
return (
|
||||
<>
|
||||
<div className="z-10 mx-auto mt-24 flex w-full max-w-xl flex-col items-center justify-center px-5">
|
||||
<Image
|
||||
src="/cat.png"
|
||||
alt="404"
|
||||
width="250"
|
||||
height="250"
|
||||
className="ml-4 rounded-sm"
|
||||
priority
|
||||
/>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-24 rounded-md border px-4 py-2 text-sm hover:border-gray-800"
|
||||
>
|
||||
Oops, Cat Not Found!
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoteNotFound() {
|
||||
return (
|
||||
<>
|
||||
<div className="z-10 mx-auto mt-24 flex w-full max-w-xl flex-col items-center justify-center px-5">
|
||||
<Image
|
||||
src="/cat.png"
|
||||
alt="404"
|
||||
width="250"
|
||||
height="250"
|
||||
className="ml-4 rounded-sm"
|
||||
priority
|
||||
/>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-24 rounded-md border px-4 py-2 text-sm hover:border-gray-800"
|
||||
>
|
||||
Oops, Cat Not Found!
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
60
apps/web/ui/layout/sign-in-modal.tsx
Normal file
60
apps/web/ui/layout/sign-in-modal.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import Modal from "@/ui/shared/modal";
|
||||
import {
|
||||
useState,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import EmailButton from "./email-login-button";
|
||||
import Link from "next/link";
|
||||
|
||||
const SignInModal = ({
|
||||
showSignInModal,
|
||||
setShowSignInModal,
|
||||
}: {
|
||||
showSignInModal: boolean;
|
||||
setShowSignInModal: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
return (
|
||||
<Modal showModal={showSignInModal} setShowModal={setShowSignInModal}>
|
||||
<div className="w-full overflow-hidden shadow-xl md:max-w-md md:rounded-2xl md:border md:border-gray-200">
|
||||
<div className="flex flex-col items-center justify-center space-y-3 border-b border-gray-200 bg-white px-4 py-5 pt-8 text-center md:px-16">
|
||||
<h3 className="font-display text-2xl font-bold">Inke</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
Please enter your email, sign in to unlock more features.{" "}
|
||||
<Link
|
||||
className="text-blue-500 after:content-['_↗'] hover:text-blue-300"
|
||||
href="/pricing"
|
||||
>
|
||||
Learn more
|
||||
</Link>{" "}
|
||||
about our plan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto flex flex-col space-y-4 bg-gray-50 px-6 pb-8 pt-2 md:px-16">
|
||||
<EmailButton />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export function useSignInModal() {
|
||||
const [showSignInModal, setShowSignInModal] = useState(false);
|
||||
|
||||
const SignInModalCallback = useCallback(() => {
|
||||
return (
|
||||
<SignInModal
|
||||
showSignInModal={showSignInModal}
|
||||
setShowSignInModal={setShowSignInModal}
|
||||
/>
|
||||
);
|
||||
}, [showSignInModal, setShowSignInModal]);
|
||||
|
||||
return useMemo(
|
||||
() => ({ setShowSignInModal, SignInModal: SignInModalCallback }),
|
||||
[setShowSignInModal, SignInModalCallback],
|
||||
);
|
||||
}
|
124
apps/web/ui/layout/user-dropdown.tsx
Normal file
124
apps/web/ui/layout/user-dropdown.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { LogOut, UserCog, ShieldCheck, Settings, Mail } from "lucide-react";
|
||||
import Popover from "@/ui/shared/popover";
|
||||
import Image from "next/image";
|
||||
import { Session } from "next-auth";
|
||||
import { generateName, greeting } from "@/lib/utils";
|
||||
import { useUserInfoByEmail } from "@/app/post/[id]/request";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function UserDropdown({
|
||||
className,
|
||||
session,
|
||||
setShowEditModal,
|
||||
}: {
|
||||
className?: string;
|
||||
session: Session;
|
||||
setShowEditModal: Dispatch<SetStateAction<boolean>>;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { email } = session?.user || {};
|
||||
const { user } = useUserInfoByEmail(session?.user?.email || "");
|
||||
const [openPopover, setOpenPopover] = useState(false);
|
||||
|
||||
if (!email) return null;
|
||||
|
||||
return (
|
||||
<div className={className + " relative inline-block text-left"}>
|
||||
<Popover
|
||||
content={
|
||||
<div className="w-full rounded-md bg-white p-2 sm:w-[270px]">
|
||||
{user && (
|
||||
<button className="relative flex w-full items-center justify-start space-x-2 rounded-md p-2 text-left text-sm transition-all duration-75 hover:bg-gray-100">
|
||||
<span className="truncate font-semibold text-slate-700">
|
||||
{greeting()}, {user?.name || `${generateName(user.id || "")}`}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<button className="relative flex w-full items-center justify-start space-x-2 rounded-md p-2 text-left text-sm transition-all duration-75 hover:bg-gray-100">
|
||||
<Mail className="h-4 w-4" />
|
||||
<span className="truncate text-sm">{email}</span>
|
||||
</button>
|
||||
|
||||
<hr className="my-2" />
|
||||
|
||||
<button
|
||||
className="relative flex w-full items-center justify-start space-x-2 rounded-md p-2 text-left text-sm transition-all duration-75 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
router.push("/pricing");
|
||||
}}
|
||||
>
|
||||
<ShieldCheck
|
||||
className={
|
||||
user?.plan !== "0"
|
||||
? "h-4 w-4 text-blue-500"
|
||||
: "h-4 w-4 text-yellow-500"
|
||||
}
|
||||
/>
|
||||
<span
|
||||
className={
|
||||
"text-sm " + (user?.plan !== "0" ? "" : "text-yellow-500")
|
||||
}
|
||||
>
|
||||
{user && (
|
||||
<span className="text-sm">
|
||||
{user?.plan !== "0" ? "Actived plan" : "Upgrade to Pro"}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="relative flex w-full items-center justify-start space-x-2 rounded-md p-2 text-left text-sm transition-all duration-75 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
setShowEditModal(true);
|
||||
setOpenPopover(false);
|
||||
}}
|
||||
>
|
||||
<UserCog className="h-4 w-4" />
|
||||
<span className="text-sm">Edit nickname</span>
|
||||
</button>
|
||||
|
||||
{/* <button
|
||||
className="relative flex w-full items-center justify-start space-x-2 rounded-md p-2 text-left text-sm transition-all duration-75 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
router.push("/settings");
|
||||
}}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
<span className="text-sm">Preferences</span>
|
||||
</button> */}
|
||||
|
||||
<hr className="my-2" />
|
||||
|
||||
<button
|
||||
className="relative flex w-full items-center justify-start space-x-2 rounded-md p-2 text-left text-sm transition-all duration-75 hover:bg-gray-100"
|
||||
onClick={() => signOut()}
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
<span className="text-sm">Sign out</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
align="end"
|
||||
openPopover={openPopover}
|
||||
setOpenPopover={setOpenPopover}
|
||||
>
|
||||
<button
|
||||
onClick={() => setOpenPopover(!openPopover)}
|
||||
className="flex h-8 w-8 items-center justify-center overflow-hidden rounded-full border border-yellow-50 transition-all duration-75 focus:outline-none active:scale-95 sm:h-9 sm:w-9"
|
||||
>
|
||||
<Image
|
||||
alt={email}
|
||||
src={user && user.image ? user.image : "/cat.png"}
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
</button>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user