Skip to content

Commit

Permalink
Ttizze/issue 80 feat (#89)
Browse files Browse the repository at this point in the history
Co-authored-by: ttizze <[email protected]>
  • Loading branch information
ttizze and ttizze authored Jul 25, 2024
1 parent 59103b3 commit add1b08
Show file tree
Hide file tree
Showing 16 changed files with 356 additions and 232 deletions.
1 change: 0 additions & 1 deletion .github/workflows/biome.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ on:
jobs:
biome-check-and-fix-for-web:
runs-on: ubuntu-latest
if: ${{ github.actor != 'github-actions[bot]' && github.event_name != 'push' || github.event.pusher.name != 'github-actions[bot]' }}
permissions:
contents: write
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/create-branch-on-issue-assign.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
esac
done
BRANCH_NAME="${ASSIGNEE,,}/issue-${ISSUE_NUMBER}-${TYPE}"
BRANCH_NAME="${ASSIGNEE,,}/${TYPE}-issue-${ISSUE_NUMBER}"
git config user.name "GitHub Actions"
git config user.email "[email protected]"
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/link-pr-to-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Link PR to Issue

on:
pull_request:
types: [opened]

jobs:
link_pr_to_issue:
runs-on: ubuntu-latest
steps:
- name: Extract issue number from branch name
id: extract-issue
run: |
BRANCH_NAME="${{ github.head_ref }}"
ISSUE_NUMBER=$(echo $BRANCH_NAME | grep -oP 'issue-\K\d+')
echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
- name: Link PR to issue
if: steps.extract-issue.outputs.issue_number
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
ISSUE_NUMBER: ${{ steps.extract-issue.outputs.issue_number }}
run: |
gh pr edit $PR_NUMBER --add-label "linked-to-issue"
gh pr comment $PR_NUMBER --body "This PR is linked to issue #$ISSUE_NUMBER"
gh issue edit $ISSUE_NUMBER --add-project "Project Board Name"
gh issue comment $ISSUE_NUMBER --body "PR #$PR_NUMBER has been linked to this issue"
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ChevronDown, ListTree, PlusCircle } from "lucide-react";
import { useMemo, useState } from "react";
import { Button } from "~/components/ui/button";
import type { TranslationWithVote } from "../types";
import { AddTranslationForm } from "./AddTranslationForm";
import { AlternativeTranslations } from "./AlternativeTranslations";
import { TranslationItem } from "./TranslationItem";

const INITIAL_DISPLAY_COUNT = 3;

export function AddAndVoteTranslations({
bestTranslationWithVote,
alternativeTranslationsWithVotes,
userId,
sourceTextId,
}: {
bestTranslationWithVote: TranslationWithVote;
alternativeTranslationsWithVotes: TranslationWithVote[];
userId: number | null;
sourceTextId: number;
}) {
const [showAll, setShowAll] = useState(false);

const displayedTranslations = useMemo(() => {
return showAll
? alternativeTranslationsWithVotes
: alternativeTranslationsWithVotes.slice(0, INITIAL_DISPLAY_COUNT);
}, [alternativeTranslationsWithVotes, showAll]);

const hasMoreTranslations =
alternativeTranslationsWithVotes.length > INITIAL_DISPLAY_COUNT;

return (
<div className="p-4 ">
<TranslationItem
translation={bestTranslationWithVote}
userId={userId}
showAuthor
/>
<div className="mt-4">
<h4 className="text-sm flex items-center justify-end gap-2">
<ListTree size={16} />
Alternative Translations
</h4>
<AlternativeTranslations
translationsWithVotes={displayedTranslations}
userId={userId}
/>
{hasMoreTranslations && !showAll && (
<Button
variant="link"
className="mt-2 w-full text-sm"
onClick={() => setShowAll(true)}
>
<ChevronDown size={16} className="mr-1" />
Show more
</Button>
)}
</div>
{userId && (
<div className="mt-4">
<h4 className="text-sm flex items-center justify-end gap-2">
<PlusCircle size={16} />
Add Your Translation
</h4>
<AddTranslationForm sourceTextId={sourceTextId} />
</div>
)}
</div>
);
}
48 changes: 14 additions & 34 deletions web/app/routes/reader.$encodedUrl/components/AddTranslationForm.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,52 @@
import { getFormProps, getTextareaProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { getZodConstraint } from "@conform-to/zod";
import { useFetcher } from "@remix-run/react";
import { Edit, Save, Trash } from "lucide-react";
import { useState } from "react";
import { Save } from "lucide-react";
import { Button } from "~/components/ui/button";
import { Textarea } from "~/components/ui/textarea";
import type { action } from "../route";
import { addTranslationSchema } from "../types";

interface AddTranslationFormProps {
sourceTextId: number;
}

export function AddTranslationForm({ sourceTextId }: AddTranslationFormProps) {
const fetcher = useFetcher();
const [isEditing, setIsEditing] = useState(false);

const fetcher = useFetcher<typeof action>();
const [form, fields] = useForm({
lastResult: fetcher.data?.lastResult,
id: `add-translation-form-${sourceTextId}`,
constraint: getZodConstraint(addTranslationSchema),
shouldValidate: "onBlur",
shouldRevalidate: "onInput",
onValidate({ formData }) {
return parseWithZod(formData, { schema: addTranslationSchema });
},
});

if (!isEditing) {
return (
<div className="mt-4 flex justify-end">
<Button
variant="outline"
size="sm"
onClick={() => setIsEditing(true)}
className="text-blue-600 hover:bg-blue-50"
>
<Edit className="h-4 w-4" />
</Button>
</div>
);
}

return (
<div className="mt-4">
<fetcher.Form method="post" {...getFormProps(form)}>
{form.errors}
<input type="hidden" name="sourceTextId" value={sourceTextId} />
<Textarea
{...getTextareaProps(fields.text)}
className="w-full mb-2 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
className="w-full mb-2 h-24 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
placeholder="Enter your translation..."
/>
{fields.text.errors && (
<p className="text-red-500">{fields.text.errors}</p>
)}
<div className="space-x-2 flex justify-end">
<div className="space-x-2 flex justify-end items-center">
{fields.text.errors && (
<p className="text-red-500">{fields.text.errors}</p>
)}
<Button
type="submit"
name="intent"
value="add"
className="bg-green-500 hover:bg-green-600 text-white"
className="bg-blue-500 hover:bg-blue-600 text-white"
disabled={fetcher.state !== "idle"}
>
<Save className="h-4 w-4" />
</Button>
<Button
variant="outline"
onClick={() => setIsEditing(false)}
className="text-red-600 hover:bg-red-50"
>
<Trash className="h-4 w-4" />
</Button>
</div>
</fetcher.Form>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { TranslationWithVote } from "../types";
import { VoteButtons } from "./VoteButtons";
import { TranslationItem } from "./TranslationItem";

interface AlternativeTranslationsProps {
translationsWithVotes: TranslationWithVote[];
Expand All @@ -13,22 +13,14 @@ export function AlternativeTranslations({
if (translationsWithVotes.length === 0) return null;

return (
<div className="rounded-md">
<p className="font-semibold text-gray-600 mb-2">Other translations:</p>
<div className="space-y-3">
{translationsWithVotes.map((translationWithVote) => (
<div
key={translationWithVote.id}
className="p-2 rounded border border-gray-200"
>
<div className="text-sm mb-2">{translationWithVote.text}</div>
<VoteButtons
translationWithVote={translationWithVote}
userId={userId}
/>
</div>
))}
</div>
<div className="space-y-3">
{translationsWithVotes.map((translation) => (
<TranslationItem
key={translation.id}
translation={translation}
userId={userId}
/>
))}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import DOMPurify from "dompurify";
import parse from "html-react-parser";
import { memo, useMemo } from "react";
import type { SourceTextInfoWithTranslations } from "../types";
import { Translation } from "./Translation";

interface ContentWithTranslationsProps {
content: string;
sourceTextInfoWithTranslations: SourceTextInfoWithTranslations[];
targetLanguage: string;
userId: number | null;
}

export const ContentWithTranslations = memo(function ContentWithTranslations({
content,
sourceTextInfoWithTranslations,
targetLanguage,
userId,
}: ContentWithTranslationsProps) {
const parsedContent = useMemo(() => {
if (typeof window === "undefined") {
return null;
}

const sanitizedContent = DOMPurify.sanitize(content);
const doc = new DOMParser().parseFromString(sanitizedContent, "text/html");
const translationMap = new Map(
sourceTextInfoWithTranslations.map((info) => [
info.number.toString(),
info,
]),
);

for (const [number] of translationMap) {
const element = doc.querySelector(`[data-number="${number}"]`);
if (element instanceof HTMLElement) {
const translationElement = doc.createElement("div");
translationElement.setAttribute("data-translation", number);
element.appendChild(translationElement);
}
}

return parse(doc.body.innerHTML, {
replace: (domNode) => {
if (domNode.type === "tag" && domNode.attribs["data-translation"]) {
const number = domNode.attribs["data-translation"];
const translationGroup = translationMap.get(number);
if (
translationGroup &&
translationGroup.translationsWithVotes.length > 0
) {
return (
<Translation
key={`translation-group-${number}`}
translationsWithVotes={translationGroup.translationsWithVotes}
targetLanguage={targetLanguage}
userId={userId}
sourceTextId={translationGroup.sourceTextId}
/>
);
}
}
return domNode;
},
});
}, [content, sourceTextInfoWithTranslations, targetLanguage, userId]);

if (typeof window === "undefined") {
return <div>Loading...</div>;
}

return <>{parsedContent}</>;
});
65 changes: 0 additions & 65 deletions web/app/routes/reader.$encodedUrl/components/TranslatedContent.tsx

This file was deleted.

Loading

0 comments on commit add1b08

Please sign in to comment.