release 0.3.4
This commit is contained in:
402
apps/web/lib/consts.ts
Normal file
402
apps/web/lib/consts.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
export const Note_Storage_Key = "note_storage_data";
|
||||
export const Content_Storage_Key = "inke__content";
|
||||
export const Content_Guide_Storage_Key = "inke__guide__content";
|
||||
export const Content_Public_Storage_Key = "inke__public__content";
|
||||
export const Default_Debounce_Duration = 750;
|
||||
|
||||
interface Plans {
|
||||
ai_generate_day: number;
|
||||
ai_generate_chars: number;
|
||||
note_upload_count: number;
|
||||
image_upload_size: number;
|
||||
ai_bot_history_length: number;
|
||||
space_user_count: number;
|
||||
pay: number;
|
||||
}
|
||||
|
||||
export const Account_Plans: Plans[] = [
|
||||
{
|
||||
// sign for free
|
||||
ai_generate_day: 100,
|
||||
ai_generate_chars: 300,
|
||||
ai_bot_history_length: 10,
|
||||
note_upload_count: 20,
|
||||
image_upload_size: 1, // mb
|
||||
space_user_count: 10,
|
||||
pay: 0,
|
||||
},
|
||||
{
|
||||
// basic
|
||||
ai_generate_day: 300,
|
||||
ai_generate_chars: 300,
|
||||
ai_bot_history_length: 32,
|
||||
note_upload_count: 10000,
|
||||
image_upload_size: 1,
|
||||
space_user_count: 10,
|
||||
pay: 0,
|
||||
},
|
||||
{
|
||||
// pro
|
||||
ai_generate_day: 1000,
|
||||
ai_generate_chars: 1000,
|
||||
ai_bot_history_length: 100,
|
||||
note_upload_count: 10000,
|
||||
image_upload_size: 10,
|
||||
space_user_count: 10,
|
||||
pay: 0,
|
||||
},
|
||||
{
|
||||
ai_generate_day: 10,
|
||||
ai_generate_chars: 10,
|
||||
ai_bot_history_length: 10,
|
||||
note_upload_count: 0,
|
||||
image_upload_size: 1,
|
||||
space_user_count: 10,
|
||||
pay: 0,
|
||||
},
|
||||
{
|
||||
ai_generate_day: 10,
|
||||
ai_generate_chars: 10,
|
||||
ai_bot_history_length: 10,
|
||||
note_upload_count: 0,
|
||||
image_upload_size: 1,
|
||||
space_user_count: 10,
|
||||
pay: 0,
|
||||
},
|
||||
{
|
||||
ai_generate_day: 50,
|
||||
ai_generate_chars: 200,
|
||||
ai_bot_history_length: 10,
|
||||
note_upload_count: 0,
|
||||
image_upload_size: 1,
|
||||
space_user_count: 10,
|
||||
pay: 0,
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultEditorContent = {
|
||||
type: "doc",
|
||||
content: [{ type: "paragraph" }],
|
||||
};
|
||||
|
||||
export const defaultEditorGuideContent = {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "heading",
|
||||
attrs: { level: 2 },
|
||||
content: [
|
||||
{ type: "text", marks: [{ type: "bold" }], text: "🎉" },
|
||||
{ type: "text", text: " " },
|
||||
{
|
||||
type: "text",
|
||||
marks: [{ type: "bold" }],
|
||||
text: "Introducing Inke",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
marks: [
|
||||
{
|
||||
type: "link",
|
||||
attrs: {
|
||||
href: "https://github.com/yesmore/inke",
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer nofollow",
|
||||
class:
|
||||
"novel-text-stone-400 novel-underline novel-underline-offset-[3px] hover:novel-text-stone-600 novel-transition-colors novel-cursor-pointer text-stone-400 underline underline-offset-[3px] hover:text-stone-600 transition-colors cursor-pointer",
|
||||
},
|
||||
},
|
||||
],
|
||||
text: "Inke",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: " is a simple-style editor with AI-powered autocompletion. With a clean and minimalist design, Inke offers a wide range of features to enhance your writing process.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: "paragraph", content: [{ type: "text", text: "Key Features:" }] },
|
||||
{
|
||||
type: "orderedList",
|
||||
attrs: { tight: true, start: 1 },
|
||||
content: [
|
||||
{
|
||||
type: "listItem",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{ type: "text", text: "😗 " },
|
||||
{
|
||||
type: "text",
|
||||
marks: [{ type: "bold" }, { type: "italic" }],
|
||||
text: "WYSIWYG Editing ",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "like markdown: Inke ensures that what you see is exactly what you get. Say goodbye to complicated formatting issues and enjoy a hassle-free editing experience. Inke offers full support for Markdown syntax with markdown shortcuts.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "listItem",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{ type: "text", text: "😄 " },
|
||||
{
|
||||
type: "text",
|
||||
marks: [{ type: "bold" }, { type: "italic" }],
|
||||
text: "Efficient Shortcut Inputs",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: ": Inke understands the importance of speed and efficiency, so it support slash menu & bubble menu.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "listItem",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{ type: "text", text: "😍 " },
|
||||
{
|
||||
type: "text",
|
||||
marks: [{ type: "bold" }, { type: "italic" }],
|
||||
text: "AI-powered Text Autocomplete",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: ": Boost your productivity with Inke's AI-powered autocomplete feature. Inke intelligently suggests completions for your text, making writing even faster and more efficient. (type ",
|
||||
},
|
||||
{ type: "text", marks: [{ type: "code" }], text: "??" },
|
||||
{
|
||||
type: "text",
|
||||
text: " to activate, or select from slash menu)",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "listItem",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{ type: "text", text: "🥰 " },
|
||||
{
|
||||
type: "text",
|
||||
marks: [{ type: "bold" }, { type: "italic" }],
|
||||
text: "Local Data Storage",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: ": Rest easy knowing that your data is safe and secure. Inke saves all your notes and documents locally, ensuring your sensitive information remains private.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "listItem",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{ type: "text", text: "🥳 " },
|
||||
{
|
||||
type: "text",
|
||||
marks: [{ type: "bold" }, { type: "italic" }],
|
||||
text: "Image uploads",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: ": drag & drop / copy & paste, or select from slash menu.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [{ type: "text", text: "Upcoming features:" }],
|
||||
},
|
||||
{
|
||||
type: "taskList",
|
||||
content: [
|
||||
{
|
||||
type: "taskItem",
|
||||
attrs: { checked: true },
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [{ type: "text", text: "Cloud storage notes" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "taskItem",
|
||||
attrs: { checked: true },
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Export notes as images",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "taskItem",
|
||||
attrs: { checked: true },
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Export notes to a local file",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
attrs: { level: 2 },
|
||||
content: [
|
||||
{ type: "text", text: "🎊 " },
|
||||
{ type: "text", marks: [{ type: "bold" }], text: "Try Inke here" },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{ type: "text", text: "Now start a new line and type " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "/" },
|
||||
{ type: "text", text: ", then you will see a " },
|
||||
{
|
||||
type: "text",
|
||||
marks: [
|
||||
{ type: "underline" },
|
||||
{ type: "textStyle", attrs: { color: "#2563EB" } },
|
||||
],
|
||||
text: "menu",
|
||||
},
|
||||
{ type: "text", text: " pop up, press the " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "↑" },
|
||||
{ type: "text", text: " or " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "↓" },
|
||||
{ type: "text", text: " to move the cursor and press " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "Enter" },
|
||||
{ type: "text", text: " to select it." },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
marks: [{ type: "underline" }],
|
||||
text: "Markdown shortcuts",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: " make it easy to format the text while typing.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{ type: "text", text: "To test that, start a new line and type " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "#" },
|
||||
{ type: "text", text: " followed by a space to get a heading. Try " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "#" },
|
||||
{ type: "text", text: ", " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "##" },
|
||||
{ type: "text", text: ", " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "###" },
|
||||
{ type: "text", text: ", " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "####" },
|
||||
{ type: "text", text: ", " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "#####" },
|
||||
{ type: "text", text: ", " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "######" },
|
||||
{ type: "text", text: " for different levels." },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{ type: "text", text: "Try " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: ">" },
|
||||
{ type: "text", text: " for blockquotes, " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "*" },
|
||||
{ type: "text", text: ", " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "-" },
|
||||
{ type: "text", text: " or " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "+" },
|
||||
{ type: "text", text: " for bullet lists, or " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "`foobar`" },
|
||||
{ type: "text", text: " to highlight code, " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "~~tildes~~" },
|
||||
{ type: "text", text: " to strike text, or " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "==equal signs==" },
|
||||
{ type: "text", text: " to highlight text." },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "You can overwrite existing input rules or add your own to nodes, marks and extensions.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{ type: "text", text: "For example, we added the " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "Typography" },
|
||||
{ type: "text", text: " extension here. Try typing " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "(c)" },
|
||||
{
|
||||
type: "text",
|
||||
text: " to see how it’s converted to a proper © character. You can also try ",
|
||||
},
|
||||
{ type: "text", marks: [{ type: "code" }], text: "->" },
|
||||
{ type: "text", text: ", " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: ">>" },
|
||||
{ type: "text", text: ", " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "1/2" },
|
||||
{ type: "text", text: ", " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "!=" },
|
||||
{ type: "text", text: ", or " },
|
||||
{ type: "text", marks: [{ type: "code" }], text: "--" },
|
||||
{ type: "text", text: "." },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
140
apps/web/lib/db/collaboration.ts
Normal file
140
apps/web/lib/db/collaboration.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { ContentItem } from "../types/note";
|
||||
import prisma from "./prisma";
|
||||
|
||||
export async function createCollaboration(
|
||||
userId: string,
|
||||
localId: string,
|
||||
roomId: string,
|
||||
title: string,
|
||||
) {
|
||||
return await prisma.collaboration.create({
|
||||
data: {
|
||||
userId,
|
||||
localId,
|
||||
roomId,
|
||||
title,
|
||||
click: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
expired: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function findCollaborationByRoomId(roomId: string, uid?: string) {
|
||||
if (uid) {
|
||||
return await prisma.collaboration.findFirst({
|
||||
where: {
|
||||
roomId,
|
||||
userId: uid,
|
||||
deletedAt: null,
|
||||
expired: null,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return await prisma.collaboration.findFirst({
|
||||
where: {
|
||||
roomId,
|
||||
// deletedAt: null,
|
||||
// expired: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
export async function findFirstCollaborationByRoomId(roomId: string) {
|
||||
return await prisma.collaboration.findFirst({
|
||||
where: {
|
||||
roomId,
|
||||
deletedAt: null,
|
||||
expired: null,
|
||||
},
|
||||
// select: {
|
||||
// userId: true,
|
||||
// roomId: true,
|
||||
// title: true,
|
||||
// createdAt: true
|
||||
|
||||
// }
|
||||
});
|
||||
}
|
||||
// 用户当前本地笔记是否已加入协作
|
||||
export async function findCollaborationBylocalId(
|
||||
localId: string,
|
||||
userId: string,
|
||||
) {
|
||||
return await prisma.collaboration.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
localId,
|
||||
deletedAt: null,
|
||||
expired: null,
|
||||
},
|
||||
select: {
|
||||
roomId: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
// 邀请中转页调用
|
||||
export async function findCollaborationByDBId(id: string) {
|
||||
return await prisma.collaboration.findFirst({
|
||||
where: {
|
||||
id,
|
||||
deletedAt: null,
|
||||
expired: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 该协作加入人数
|
||||
export async function findCollaborationInviteCount(id: string) {
|
||||
return await prisma.collaboration.count({
|
||||
where: {
|
||||
roomId: id,
|
||||
deletedAt: null,
|
||||
expired: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
// 用户所有参与的协作分享
|
||||
export async function findUserCollaborations(userId: string) {
|
||||
return await prisma.collaboration.findMany({
|
||||
where: {
|
||||
userId,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// export async function updateCollaboration(click: number, id: string) {
|
||||
// return await prisma.collaboration.update({
|
||||
// where: {
|
||||
// id,
|
||||
// },
|
||||
// data: {
|
||||
// click,
|
||||
// updatedAt: new Date(),
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
export async function updateCollaborationClick(id: string, pre: number) {
|
||||
return await prisma.collaboration.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
click: pre + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteCollaborationNote(id: string) {
|
||||
return await prisma.collaboration.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
11
apps/web/lib/db/prisma.ts
Normal file
11
apps/web/lib/db/prisma.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
declare global {
|
||||
var prisma: PrismaClient | undefined;
|
||||
}
|
||||
|
||||
const prisma = global.prisma || new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV === "development") global.prisma = prisma;
|
||||
|
||||
export default prisma;
|
103
apps/web/lib/db/share.ts
Normal file
103
apps/web/lib/db/share.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { ContentItem } from "../types/note";
|
||||
import prisma from "./prisma";
|
||||
|
||||
export async function createShareNote(json: ContentItem, uid: string) {
|
||||
return await prisma.shareNote.create({
|
||||
data: {
|
||||
userId: uid,
|
||||
localId: json.id,
|
||||
data: JSON.stringify(json),
|
||||
click: 0,
|
||||
keeps: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function findShareByLocalId(id: string, uid?: string) {
|
||||
if (uid) {
|
||||
return await prisma.shareNote.findFirst({
|
||||
where: {
|
||||
localId: id,
|
||||
userId: uid,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return await prisma.shareNote.findFirst({
|
||||
where: {
|
||||
localId: id,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
export async function findShareByDBId(id: string) {
|
||||
return await prisma.shareNote.findFirst({
|
||||
where: {
|
||||
id,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
export async function findUserSharesCount(uid: string) {
|
||||
return await prisma.shareNote.count({
|
||||
where: {
|
||||
userId: uid,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
export async function findUserShares(uid: string) {
|
||||
return await prisma.shareNote.findMany({
|
||||
where: {
|
||||
userId: uid,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateShareNote(json: ContentItem, id: string) {
|
||||
return await prisma.shareNote.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
data: JSON.stringify(json),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
export async function updateShareClick(id: string, pre: number) {
|
||||
return await prisma.shareNote.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
click: pre + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
export async function updateShareKeeps(id: string, pre: number) {
|
||||
return await prisma.shareNote.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
keeps: pre + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteShareNote(id: string) {
|
||||
return await prisma.shareNote.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
23
apps/web/lib/db/user.ts
Normal file
23
apps/web/lib/db/user.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import prisma from "./prisma";
|
||||
|
||||
// get all users from user schema
|
||||
export const getUsers = async () => {
|
||||
return await prisma.user.findMany();
|
||||
};
|
||||
|
||||
export const getUserByEmail = async (email: string) => {
|
||||
return await prisma.user.findFirst({
|
||||
where: { email },
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserById = async (id: string) => {
|
||||
return await prisma.user.findFirst({
|
||||
where: { id },
|
||||
select: { name: true, image: true },
|
||||
});
|
||||
};
|
||||
|
||||
export const updateUserName = async (id: string, name: string) => {
|
||||
return await prisma.user.update({ data: { name }, where: { id } });
|
||||
};
|
16
apps/web/lib/hooks/use-scroll.ts
Normal file
16
apps/web/lib/hooks/use-scroll.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export default function useScroll(threshold: number) {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
const onScroll = useCallback(() => {
|
||||
setScrolled(window.pageYOffset > threshold);
|
||||
}, [threshold]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("scroll", onScroll);
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, [onScroll]);
|
||||
|
||||
return scrolled;
|
||||
}
|
38
apps/web/lib/hooks/use-window-size.ts
Normal file
38
apps/web/lib/hooks/use-window-size.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function useWindowSize() {
|
||||
const [windowSize, setWindowSize] = useState<{
|
||||
width: number | undefined;
|
||||
height: number | undefined;
|
||||
}>({
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Handler to call on window resize
|
||||
function handleResize() {
|
||||
// Set window width/height to state
|
||||
setWindowSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listener
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
// Call handler right away so state gets updated with initial window size
|
||||
handleResize();
|
||||
|
||||
// Remove event listener on cleanup
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []); // Empty array ensures that effect is only run on mount
|
||||
|
||||
return {
|
||||
windowSize,
|
||||
isMobile: typeof windowSize?.width === "number" && windowSize?.width < 768,
|
||||
isDesktop:
|
||||
typeof windowSize?.width === "number" && windowSize?.width >= 768,
|
||||
};
|
||||
}
|
8
apps/web/lib/types/active-code.ts
Normal file
8
apps/web/lib/types/active-code.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface ActiveCodeItem {
|
||||
id: string;
|
||||
userId: string;
|
||||
code: string;
|
||||
expires: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
21
apps/web/lib/types/note.ts
Normal file
21
apps/web/lib/types/note.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { JSONContent } from "@tiptap/react";
|
||||
|
||||
export interface ContentItem {
|
||||
id: string;
|
||||
title: string;
|
||||
content: JSONContent;
|
||||
tag?: string;
|
||||
created_at?: number;
|
||||
updated_at?: number;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
export interface ShareNoteItem {
|
||||
id: string;
|
||||
userId: string;
|
||||
localId: string;
|
||||
data: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
deletedAt?: number;
|
||||
}
|
5
apps/web/lib/types/response.ts
Normal file
5
apps/web/lib/types/response.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface IResponse<T> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
10
apps/web/lib/types/user.ts
Normal file
10
apps/web/lib/types/user.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface User {
|
||||
id?: string;
|
||||
name?: string;
|
||||
email: string;
|
||||
emailVerified: string;
|
||||
image?: string;
|
||||
credit: number;
|
||||
active: number;
|
||||
plan: string;
|
||||
}
|
@@ -1,6 +1,166 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import ms from "ms";
|
||||
import { ContentItem } from "./types/note";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export const timeAgo = (timestamp: number, timeOnly?: boolean): string => {
|
||||
if (!timestamp) return "never";
|
||||
return `${ms(Date.now() - new Date(timestamp).getTime())}${
|
||||
timeOnly ? "" : " ago"
|
||||
}`;
|
||||
};
|
||||
|
||||
export async function fetcher<JSON = any>(
|
||||
input: RequestInfo,
|
||||
init?: RequestInit,
|
||||
): Promise<JSON> {
|
||||
const res = await fetch(input, init);
|
||||
|
||||
if (!res.ok) {
|
||||
const json = await res.json();
|
||||
if (json.error) {
|
||||
const error = new Error(json.error) as Error & {
|
||||
status: number;
|
||||
};
|
||||
error.status = res.status;
|
||||
throw error;
|
||||
} else {
|
||||
throw new Error("An unexpected error occurred");
|
||||
}
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export function nFormatter(num: number, digits?: number) {
|
||||
if (!num) return "0";
|
||||
const lookup = [
|
||||
{ value: 1, symbol: "" },
|
||||
{ value: 1e3, symbol: "K" },
|
||||
{ value: 1e6, symbol: "M" },
|
||||
{ value: 1e9, symbol: "G" },
|
||||
{ value: 1e12, symbol: "T" },
|
||||
{ value: 1e15, symbol: "P" },
|
||||
{ value: 1e18, symbol: "E" },
|
||||
];
|
||||
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
|
||||
var item = lookup
|
||||
.slice()
|
||||
.reverse()
|
||||
.find(function (item) {
|
||||
return num >= item.value;
|
||||
});
|
||||
return item
|
||||
? (num / item.value).toFixed(digits || 1).replace(rx, "$1") + item.symbol
|
||||
: "0";
|
||||
}
|
||||
|
||||
export function isEmail(str: string) {
|
||||
const reg = /^([a-zA-Z\d._%+-]+)@([a-zA-Z\d.-]+\.[a-zA-Z]{2,})$/;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
export function getAvatarById(id: string) {
|
||||
return `https://avatars.dicebear.com/api/micah/${id}.svg`;
|
||||
}
|
||||
|
||||
export function formatDate(dateString: string) {
|
||||
if (!dateString) {
|
||||
return `0秒前`;
|
||||
}
|
||||
const sourceDate = new Date(dateString).getTime();
|
||||
const currentDate = new Date().getTime();
|
||||
|
||||
const timeDiff = currentDate - sourceDate;
|
||||
const secondsDiff = Math.floor(timeDiff / 1000); // 计算秒数差
|
||||
|
||||
if (secondsDiff < 60) {
|
||||
return `${secondsDiff}秒前`;
|
||||
} else if (secondsDiff < 3600) {
|
||||
// 不足1小时
|
||||
const minutesDiff = Math.floor(secondsDiff / 60);
|
||||
return `${minutesDiff}分钟前`;
|
||||
} else if (secondsDiff < 86400) {
|
||||
// 不足1天
|
||||
const hoursDiff = Math.floor(secondsDiff / 3600);
|
||||
return `${hoursDiff}小时前`;
|
||||
} else {
|
||||
const daysDiff = Math.floor(secondsDiff / 86400);
|
||||
return `${daysDiff}天前`;
|
||||
}
|
||||
}
|
||||
|
||||
export function fomatTmpDate(dateNum: number) {
|
||||
const date = new Date(dateNum);
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`;
|
||||
}
|
||||
|
||||
export function generateName(id: string) {
|
||||
return `u-${id.slice(-6)}`;
|
||||
}
|
||||
|
||||
export function exportAsJson(data: any, filename: string) {
|
||||
const dataStr = JSON.stringify(data);
|
||||
const dataUri =
|
||||
"data:application/json;charset=utf-8," + encodeURIComponent(dataStr);
|
||||
|
||||
const linkElement = document.createElement("a");
|
||||
linkElement.setAttribute("href", dataUri);
|
||||
linkElement.setAttribute("download", filename + ".json");
|
||||
linkElement.style.display = "none";
|
||||
document.body.appendChild(linkElement);
|
||||
|
||||
linkElement.click();
|
||||
document.body.removeChild(linkElement);
|
||||
}
|
||||
|
||||
export function exportAsMarkdownFile(data: string, fileName: string) {
|
||||
const dataUri = "data:text/plain;charset=utf-8," + encodeURIComponent(data);
|
||||
|
||||
const linkElement = document.createElement("a");
|
||||
linkElement.setAttribute("href", dataUri);
|
||||
linkElement.setAttribute("download", fileName + ".md");
|
||||
linkElement.style.display = "none";
|
||||
document.body.appendChild(linkElement);
|
||||
|
||||
linkElement.click();
|
||||
document.body.removeChild(linkElement);
|
||||
}
|
||||
|
||||
export function getRandomElement(arr: string[]) {
|
||||
let currentIndex = arr.length;
|
||||
let temporaryValue;
|
||||
let randomIndex;
|
||||
|
||||
// 当还有未洗牌的元素时
|
||||
while (currentIndex !== 0) {
|
||||
// 从剩余的元素中随机选择一个元素
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex -= 1;
|
||||
|
||||
// 将当前元素与随机选择的元素进行交换
|
||||
temporaryValue = arr[currentIndex];
|
||||
arr[currentIndex] = arr[randomIndex];
|
||||
arr[randomIndex] = temporaryValue;
|
||||
}
|
||||
|
||||
// 返回洗牌后的数组的第一个元素
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
export function greeting() {
|
||||
var currentTime = new Date();
|
||||
var currentHour = currentTime.getHours();
|
||||
|
||||
if (currentHour < 12) {
|
||||
return "上午好";
|
||||
} else if (currentHour < 18) {
|
||||
return "下午好";
|
||||
} else {
|
||||
return "晚上好";
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user