1
0
Code Issues Pull Requests Packages Projects Releases Wiki Activity GitHub Gitee

release 0.3.4

This commit is contained in:
songjunxi
2023-11-10 14:59:47 +08:00
parent 2b456637e9
commit 602f2059fd
161 changed files with 9921 additions and 347 deletions

402
apps/web/lib/consts.ts Normal file
View 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 its 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: "." },
],
},
],
};

View 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
View 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
View 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
View 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 } });
};

View 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;
}

View 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,
};
}

View File

@@ -0,0 +1,8 @@
export interface ActiveCodeItem {
id: string;
userId: string;
code: string;
expires: string;
createdAt: string;
updatedAt: string;
}

View 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;
}

View File

@@ -0,0 +1,5 @@
export interface IResponse<T> {
code: number;
msg: string;
data: T;
}

View File

@@ -0,0 +1,10 @@
export interface User {
id?: string;
name?: string;
email: string;
emailVerified: string;
image?: string;
credit: number;
active: number;
plan: string;
}

View File

@@ -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 "晚上好";
}
}