Skip to content

Commit 1768772

Browse files
authored
feat(word-editor): implement edit word action (#32)
This Pull Request implements the edit functionality for word on the word editor. The edit operation takes the current change made to a word on submit, perform a submit operation through the `handleSubmitWord` handler and routes to the `/editor` page after successful edit operation. ### Changes Made - Added the dynamic word edit route at `src/pages/editor/edit/[word].astro`; this route dynamically take the word found on the route `word` param and operates with it - Implemented the `doEditWord` action; which retrieves the current edit word's `word` param and fetches an existing word (record) in dictionary for the word using the `getExistingWord` function from the `word-editor` script, and returns an object of the word ```js /** * Edit Word action - meant to be executed on `editor/edit/[word]` route * @param {import("astro").AstroGlobal} astroGlobal */ export default async function doEditWord(astroGlobal) { const { cookies, params: { word } } = astroGlobal; const accessToken = cookies.get("jargons.dev:token", { decode: value => decrypt(value) }); const userOctokit = app.getUserOctokit({ token: accessToken.value }); const response = await getExistingWord(userOctokit, PROJECT_REPO_DETAILS, word); return response; } ``` - Added a new props `action` to the `WordEditor` component,... - `action` - this prop accepts either `new` or `edit` as value - `metadata` - props to hold other words data from github api response - `octokitAuths` props that hold authToken for current authenticated user and jargons.dev app - Added `gray-matter` to deps, used for parsing the content of the `word.content_decoded` property allowing separation of the `title` and `content` from the property which will then be passed as existing value to the `WordEditor` component (from the `word-editor` island) - Implemented the `resolveEditorActionFromPathname` utils function which is used to compute the current word editor action from the current url pathname which is `/editor/edit/[word]` for word edit and will be `/editor/new` for new word addition - Replace the `Editor` component (on `word-editor` island) wrapper `div` with a `form` element - Added a new `SubmitButton` component within the `word-editor`; exported to be integrated as a separate island that is linked to the word editor `form`; this component integrates a button that acts as the submit button for the word editor `form`; on click this button, the editor form submits - Added a new boolean `$isWordSubmitLoading` state to `dictionary` store for tracking when a word submission is in action - Integrated the `SubmitButton` into the `Navbar` component with a `client:load` directive in order to enable interactivity for submission loading state - Implemented the `handleSubmitWord` handler function; this function currently handle the word `edit` operation (should handle `new` word addition next); it takes the required `octokitAuth` tokens(this is the user's authToken and the jargons.dev app authToken - it uses this to create an octokit instance for the user and app respectively for consumption in all other function execution that requires them as param), `action` and `word` data; and handles all the process of contribution by doing the following.... - Creates a fork on the current users' account - Creates a new branch to commit change to - Commits the changes to the new branch - Create a PR to merge this branch to the main project repo - Implemented the `doOctokitAuth` action which compute and avails the authToken for currently authenticated user and the jargons.dev app; which are consumed by the `handleSubmitWord` handler - Extracted the `devJargonsOctokit` integration in the `submit-word` script; replacing it with a param ensuring that the script's `submitWord` function accepts the `devJargonsOctokit` as param; this improves on the implementation at https://github.com/babblebey/jargons.dev/pull/25 - Added a new `option` object param to the `updateExistingWord` and `writeNewWord` with `env` property that accepts either `node` or `browser`; this option is used to determine which encoding function/api is used to encode the `content` for file write operation, i.e. `Buffer` in node and `btoa` in browser or client-side ### Screencast https://github.com/babblebey/jargons.dev/assets/25631971/4352565a-a38d-4e79-b93b-001263ff2456
1 parent d775b92 commit 1768772

18 files changed

Lines changed: 417 additions & 25 deletions

File tree

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ GITHUB_OAUTH_APP_CLIENT_SECRET=""
88

99
CRYPTO_SECRET_KEY="secret"
1010

11-
PROJECT_REPO="babblebey/jargons.dev"
12-
PROJECT_REPO_BRANCH_REF="refs/heads/main"
11+
PUBLIC_PROJECT_REPO="babblebey/jargons.dev"
12+
PUBLIC_PROJECT_REPO_BRANCH_REF="refs/heads/main"

constants.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const PROJECT_REPO_DETAILS = {
2+
repoFullname: import.meta.env.PUBLIC_PROJECT_REPO,
3+
repoMainBranchRef: import.meta.env.PUBLIC_PROJECT_REPO_BRANCH_REF
4+
}

package-lock.json

Lines changed: 147 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
"@fontsource/ibm-plex-mono": "^5.0.12",
2222
"@fontsource/inter": "^5.0.17",
2323
"@nanostores/react": "^0.7.2",
24+
"@octokit-next/core": "^2.8.0",
2425
"@octokit/auth-oauth-app": "^8.1.0",
2526
"@octokit/oauth-authorization-url": "^7.1.0",
2627
"@types/react": "^18.2.69",
2728
"@types/react-dom": "^18.2.22",
2829
"astro": "^4.5.9",
2930
"flexsearch": "^0.7.43",
31+
"gray-matter": "^4.0.3",
3032
"nanostores": "^0.10.0",
3133
"octokit": "^3.1.2",
3234
"react": "^18.2.0",

src/components/islands/word-editor.jsx

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,89 @@
11
import { useEffect } from "react";
22
import Markdown from "react-markdown";
3+
import { useStore } from "@nanostores/react";
4+
import useRouter from "../../lib/hooks/use-router.js";
35
import useWordEditor from "../../lib/hooks/use-word-editor.js";
6+
import { $isWordSubmitLoading } from "../../lib/stores/dictionary.js";
7+
import handleSubmitWord from "../../lib/handlers/handle-submit-word.js";
48

5-
export default function WordEditor({ title = "", content = "" }) {
9+
export default function WordEditor({ title = "", content = "", metadata = {}, action, octokitAuths }) {
610
return (
711
<div className="w-full flex border rounded-lg">
8-
<Editor
12+
<Editor
13+
action={action}
914
eTitle={title}
1015
eContent={content}
16+
eMetadata={metadata}
17+
octokitAuths={octokitAuths}
18+
submitHandler={handleSubmitWord}
1119
className="w-full h-full flex flex-col p-5 border-r"
1220
/>
1321
<Preview className="w-full h-full flex flex-col p-5" />
1422
</div>
1523
);
1624
}
1725

18-
function Editor({ eTitle, eContent, className, ...props }) {
26+
export function SubmitButton({ children = "Submit" }) {
27+
const isSubmitLoading = useStore($isWordSubmitLoading);
28+
29+
return (
30+
<button className="flex items-center justify-center no-underline text-white bg-gray-900 hover:bg-gray-700 focus:ring-0 font-medium rounded-lg text-base px-5 py-2.5 text-center ml-1 sm:ml-3"
31+
type="submit"
32+
form="jargons.dev:word_editor"
33+
disabled={isSubmitLoading}
34+
>
35+
{ isSubmitLoading ? (
36+
<div className="flex-none h-4 w-4 md:w-6 md:h-6 rounded-full border-2 border-gray-400 border-b-gray-200 border-r-gray-200 animate-spin" />
37+
) : (
38+
children
39+
) }
40+
</button>
41+
);
42+
}
43+
44+
function Editor({ eTitle, eContent, eMetadata, className, submitHandler, action, octokitAuths, ...props }) {
45+
const router = useRouter();
1946
const { title, setTitle, content, setContent } = useWordEditor();
2047

2148
useEffect(() => {
2249
setTitle(eTitle);
2350
setContent(eContent);
2451
}, []);
2552

53+
async function handleOnSubmit() {
54+
$isWordSubmitLoading.set(true);
55+
await submitHandler(octokitAuths, action, {
56+
title,
57+
content,
58+
metadata: eMetadata
59+
});
60+
router.push("/editor");
61+
}
62+
2663
return (
27-
<div
64+
<form
2865
className={`${className} relative`}
66+
onSubmit={(e) => {
67+
e.preventDefault();
68+
handleOnSubmit();
69+
}}
70+
id="jargons.dev:word_editor"
2971
{...props}
3072
>
3173
<input
32-
className="block w-full pb-2 mb-3 text-gray-900 border-b text-lg font-bold focus:outline-none"
74+
className={`${action === "edit" && "cursor-not-allowed"} block w-full pb-2 mb-3 text-gray-900 border-b text-lg font-bold focus:outline-none`}
3375
type="text"
3476
placeholder="New Word"
3577
value={title}
78+
readOnly={action === "edit"}
3679
onChange={(e) => setTitle(e.target.value)}
3780
/>
3881
<textarea
3982
className="w-full grow resize-none appearance-none border-none focus:outline-none scrollbar"
4083
value={content}
4184
onChange={(e) => setContent(e.target.value)}
4285
/>
43-
</div>
86+
</form>
4487
);
4588
}
4689

src/lib/actions/do-auth.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default async function doAuth(astroGlobal) {
2525
if (!isStateEmpty(state)){
2626
if (state.path) parsedState += `path:${state.path}`;
2727
const otherStates = String(Object.keys(state)
28-
.filter(key => key !== "path" && key !== "redirect")
28+
.filter(key => key !== "path")
2929
.map(key => key + ":" + state[key]).join("|"));
3030
if (otherStates.length > 0) parsedState += `|${otherStates}`;
3131
}
@@ -38,7 +38,7 @@ export default async function doAuth(astroGlobal) {
3838
}
3939

4040
try {
41-
if (!accessToken && code) {
41+
if (code) {
4242
const response = await getAuthorization(astroGlobal);
4343
const auth = await response.json();
4444

src/lib/actions/do-edit-word.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import app from "../octokit/app.js";
2+
import { getExistingWord } from "../word-editor.js";
3+
import { PROJECT_REPO_DETAILS } from "../../../constants.js";
4+
5+
/**
6+
* Edit Word action - meant to be executed on `editor/edit/[word]` route
7+
* @param {import("astro").AstroGlobal} astroGlobal
8+
*/
9+
export default async function doEditWord(astroGlobal) {
10+
const { cookies, params: { word } } = astroGlobal;
11+
12+
const accessToken = cookies.get("jargons.dev:token", { decode: value => decrypt(value) });
13+
const userOctokit = app.getUserOctokit({ token: accessToken.value });
14+
15+
const response = await getExistingWord(userOctokit, PROJECT_REPO_DETAILS, word);
16+
17+
return response;
18+
}

src/lib/actions/do-octokit-auth.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import app from "../octokit/app.js";
2+
import { decrypt } from "../utils/crypto.js";
3+
4+
/**
5+
* Resolve octokit Auth tokens for current user and devJargons App
6+
* @param {import("astro").AstroGlobal} astroGlobal
7+
*
8+
* @todo ideally: encrypt the `tokens` for later decryption on client-side
9+
* @todo i.e. remove `decode` operation from the `userAuthToken` cookie data fetch
10+
* @see all todos can be resolved here: https://github.com/babblebey/jargons.dev/issues/37
11+
*/
12+
export default function doOctokitAuth({ cookies }) {
13+
const userAuthToken = cookies.get("jargons.dev:token", {
14+
decode: value => decrypt(value)
15+
});
16+
17+
return {
18+
user: userAuthToken.value,
19+
devJargons: app.devJargonsAppAuth
20+
}
21+
}

src/lib/fork.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getRepoParts } from "./utils/index.js";
55
* Fork the project (specified) Repo to user account
66
* @param {import("octokit").Octokit} userOctokit
77
* @param {{ repoFullname: string, repoMainBranchRef: string }} projectRepoDetails
8-
* @returns {string} fullname of forked repo - [userlogin]/jargons.dev
8+
* @returns {Promise<string>} fullname of forked repo - [userlogin]/jargons.dev
99
*/
1010
export async function forkRepository(userOctokit, projectRepoDetails) {
1111
const { repoFullname, repoMainBranchRef } = projectRepoDetails;

0 commit comments

Comments
 (0)