Skip to content

Refactor article handle generation logic in article.actions.ts to sup… #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/app/api/play/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { slugify } from "@/lib/slug-helper.util";
import * as articleActions from "@/backend/services/article.actions";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
// const _headers = await headers();
return NextResponse.json({
slug: slugify("কেমন আছেন আপনারা?"),
handle: await articleActions.getUniqueArticleHandle(
"untitled",
"fc6cfc91-f017-4923-9706-8813ae8df621"
),
});
}
88 changes: 80 additions & 8 deletions src/backend/services/article.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
desc,
eq,
joinTable,
like,
neq,
or,
} from "../persistence/persistence-where-operator";
import { PersistentRepository } from "../persistence/persistence.repository";
import {
Expand Down Expand Up @@ -84,18 +86,86 @@ export async function createMyArticle(
}
}

export const getUniqueArticleHandle = async (title: string) => {
export const getUniqueArticleHandle = async (
title: string,
ignoreArticleId?: string
) => {
try {
const count = await articleRepository.findRowCount({
where: eq("handle", slugify(title)),
columns: ["id", "handle"],
// Slugify the title first
const baseHandle = slugify(title);

// If we have an ignoreArticleId, check if this article already exists
if (ignoreArticleId) {
const [existingArticle] = await articleRepository.findRows({
where: eq("id", ignoreArticleId),
columns: ["id", "handle"],
limit: 1,
});

// If the article exists and its handle is already the slugified title,
// we can just return that handle (no need to append a number)
if (existingArticle && existingArticle.handle === baseHandle) {
return baseHandle;
}
}

// Find all articles with the same base handle or handles that have numeric suffixes
const handlePattern = `${baseHandle}-%`;
let baseHandleWhereClause: any = eq<Article, keyof Article>(
"handle",
baseHandle
);
let suffixWhereClause: any = like<Article>("handle", handlePattern);

let whereClause: any = or(baseHandleWhereClause, suffixWhereClause);

if (ignoreArticleId) {
whereClause = and(
whereClause,
neq<Article, keyof Article>("id", ignoreArticleId)
);
}

// Get all existing handles that match our patterns
const existingArticles = await articleRepository.findRows({
where: whereClause,
columns: ["handle"],
});
if (count) {
return `${slugify(title)}-${count + 1}`;

// If no existing handles found, return the base handle
if (existingArticles.length === 0) {
return baseHandle;
}
return slugify(title);

// Check if the exact base handle exists
const exactBaseExists = existingArticles.some(
(article) => article.handle === baseHandle
);

// If the exact base handle doesn't exist, we can use it
if (!exactBaseExists) {
return baseHandle;
}

// Find the highest numbered suffix
let highestNumber = 1;
const regex = new RegExp(`^${baseHandle}-(\\d+)$`);

existingArticles.forEach((article) => {
const match = article.handle.match(regex);
if (match) {
const num = parseInt(match[1], 10);
if (num >= highestNumber) {
highestNumber = num + 1;
}
}
});

// Return with the next number in sequence
return `${baseHandle}-${highestNumber}`;
} catch (error) {
handleRepositoryException(error);
throw error;
}
};

Expand All @@ -116,7 +186,9 @@ export async function updateArticle(
where: eq("id", input.article_id),
data: {
title: input.title,
handle: input.handle,
handle: input.handle
? await getUniqueArticleHandle(input.handle, input.article_id)
: undefined,
excerpt: input.excerpt,
body: input.body,
cover_image: input.cover_image,
Expand Down
48 changes: 29 additions & 19 deletions src/components/Editor/ArticleEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,14 @@ const ArticleEditor: React.FC<Prop> = ({ article, uuid }) => {
const appConfig = useAppConfirm();
const titleRef = useRef<HTMLTextAreaElement>(null!);
const bodyRef = useRef<HTMLTextAreaElement | null>(null);
const setDebouncedTitle = useDebouncedCallback(() => handleSaveTitle(), 1000);
const setDebouncedBody = useDebouncedCallback(() => handleSaveBody(), 1000);
const setDebouncedTitle = useDebouncedCallback(
(title: string) => handleDebouncedSaveTitle(title),
1000
);
const setDebouncedBody = useDebouncedCallback(
(body: string) => handleDebouncedSaveBody(body),
1000
);

const [editorMode, selectEditorMode] = React.useState<"write" | "preview">(
"write"
Expand Down Expand Up @@ -91,39 +97,43 @@ const ArticleEditor: React.FC<Prop> = ({ article, uuid }) => {
},
});

const handleSaveTitle = () => {
const handleSaveArticleOnBlurTitle = (title: string) => {
if (!uuid) {
if (editorForm.watch("title")) {
if (title) {
articleCreateMutation.mutate({
title: editorForm.watch("title") ?? "",
title: title ?? "",
});
}
}
};

const handleDebouncedSaveTitle = (title: string) => {
if (uuid) {
if (editorForm.watch("title")) {
if (title) {
updateMyArticleMutation.mutate({
title: title ?? "",
article_id: uuid,
title: editorForm.watch("title") ?? "",
});
}
}
};

const handleSaveBody = () => {
// if (!uuid) {
// if (editorForm.watch("body")) {
// articleCreateMutation.mutate({
// title: editorForm.watch("body") ?? "",
// });
// }
// }

const handleDebouncedSaveBody = (body: string) => {
if (uuid) {
if (editorForm.watch("body")) {
if (body) {
updateMyArticleMutation.mutate({
article_id: uuid,
body: editorForm.watch("body") ?? "",
handle: article?.handle ?? "untitled",
body,
});
}
} else {
if (body) {
articleCreateMutation.mutate({
title: editorForm.watch("title")?.length
? (editorForm.watch("title") ?? "untitled")
: "untitled",
body,
});
}
}
Expand Down Expand Up @@ -229,7 +239,7 @@ const ArticleEditor: React.FC<Prop> = ({ article, uuid }) => {
value={editorForm.watch("title")}
className="w-full text-2xl focus:outline-none bg-background resize-none"
ref={titleRef}
onBlur={() => handleSaveTitle()}
onBlur={(e) => handleSaveArticleOnBlurTitle(e.target.value)}
onChange={(e) => {
editorForm.setValue("title", e.target.value);
setDebouncedTitle(e.target.value);
Expand Down
2 changes: 2 additions & 0 deletions src/components/Editor/ArticleEditorDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ const ArticleEditorDrawer: React.FC<Props> = ({ article, open, onClose }) => {
onSubmit={form.handleSubmit(handleOnSubmit)}
className="flex flex-col gap-2"
>
{JSON.stringify(form.formState.errors)}

<FormField
control={form.control}
name="handle"
Expand Down