Skip to content

Commit a45edde

Browse files
committed
/contribute/:id page
1 parent 4a2d271 commit a45edde

File tree

8 files changed

+237
-44
lines changed

8 files changed

+237
-44
lines changed

api/src/app/endpoints.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GetContributionsResponse } from "src/contribution/types";
1+
import { GetContributionResponse, GetContributionsResponse } from "src/contribution/types";
22
import {
33
GetContributorNameResponse,
44
GetContributorResponse,
@@ -33,6 +33,10 @@ export interface Endpoints {
3333
"api:Contributions": {
3434
response: GetContributionsResponse;
3535
};
36+
"api:Contributions/:id": {
37+
response: GetContributionResponse;
38+
params: { id: string };
39+
};
3640
"api:Contributors": {
3741
response: GetContributorsResponse;
3842
};

web/src/_entry/app.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ let routes: Array<
3434
},
3535
{
3636
pageName: "contribute",
37-
// @TODO-ZM: change this back once we have contribution page
38-
path: "/contribute/:slug?",
37+
path: "/contribute",
38+
},
39+
{
40+
pageName: "contribute/contribution",
41+
path: "/contribute/*",
3942
},
4043
{
4144
pageName: "team",

web/src/components/locale/dictionary.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,18 @@ Besides the open tasks on [/Contribute](/Contribute) page, you can also contribu
356356
en: "Review changes",
357357
ar: "مراجعة التغييرات",
358358
},
359+
"contribution-title-pre": {
360+
en: "Help with: ",
361+
ar: "ساعد في: ",
362+
},
363+
"contribution-title-post": {
364+
en: " | DzCode i/o",
365+
ar: " | DzCode i / o",
366+
},
367+
"contribution-breadcrumbs-1": {
368+
en: "Contributions",
369+
ar: "المساهمات",
370+
},
359371
"elapsed-time-suffixes": {
360372
en: "y|mo|d|h|min|Just now",
361373
ar: " عام| شهر| يوم| ساعة| دقيقة| الآن",
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import React, { useEffect, useMemo } from "react";
2+
import { useAppDispatch, useAppSelector } from "src/redux/store";
3+
import { useParams } from "react-router-dom";
4+
import { Redirect } from "src/components/redirect";
5+
import { fetchContributionAction } from "src/redux/actions/contribution";
6+
import { Helmet } from "react-helmet-async";
7+
import { Locale, useLocale } from "src/components/locale";
8+
import { getContributionURL } from "src/utils/contribution";
9+
import { Link } from "src/components/link";
10+
import { TryAgain } from "src/components/try-again";
11+
import { Loading } from "src/components/loading";
12+
import { Markdown } from "src/components/markdown";
13+
import { getElapsedTime } from "src/utils/elapsed-time";
14+
15+
// ts-prune-ignore-next
16+
export default function Page(): JSX.Element {
17+
const { localize } = useLocale();
18+
const { contribution } = useAppSelector((state) => state.contributionPage);
19+
const dispatch = useAppDispatch();
20+
const { "*": contributionSlug } = useParams<{ "*": string }>();
21+
const contributionId = useMemo(() => {
22+
// slug: [title slug]-[id: [provider]-[number]]
23+
const id = contributionSlug?.split("-").slice(-2).join("-");
24+
return id;
25+
}, [contributionSlug]);
26+
27+
useEffect(() => {
28+
dispatch(fetchContributionAction(contributionId));
29+
}, [dispatch, contributionId]);
30+
31+
if (contribution === "404") {
32+
return <Redirect href="/contribute" />;
33+
}
34+
35+
return (
36+
<main className="flex flex-col self-center w-full max-w-7xl">
37+
{contribution !== "ERROR" && contribution !== null ? (
38+
<Helmet>
39+
<title>
40+
{localize("contribution-title-pre")} {contribution.title}{" "}
41+
{localize("contribution-title-post")}
42+
</title>
43+
<meta name="description" content={localize("contribute-description")} />
44+
{/* @TODO-ZM: add canonical url on all pages */}
45+
<link rel="canonical" href={getContributionURL(contribution)} />
46+
</Helmet>
47+
) : null}
48+
<div className="breadcrumbs p-4">
49+
<ul>
50+
<li>
51+
<Link className="link" href="/contribute">
52+
<Locale contribution-breadcrumbs-1 />
53+
</Link>
54+
</li>
55+
{contribution !== "ERROR" && contribution !== null ? <li>{contribution.title}</li> : null}
56+
</ul>
57+
</div>
58+
<div className="flex flex-col self-center w-full max-w-4xl">
59+
{contribution === "ERROR" ? (
60+
<TryAgain
61+
error={localize("global-generic-error")}
62+
action={localize("global-try-again")}
63+
onClick={() => {
64+
dispatch(fetchContributionAction(contributionId));
65+
}}
66+
/>
67+
) : contribution === null ? (
68+
<Loading />
69+
) : (
70+
<div className="flex flex-col gap-4 items-center p-4">
71+
{/* TODO-ZM: more tailored design for /contribute/:slug page instead of copy-pasting components from /contribute */}
72+
<div
73+
dir="ltr"
74+
className="card card-compact bg-base-300 flex-auto w-full max-w-xs sm:max-w-sm"
75+
>
76+
<div className="card-body markdown">
77+
<div className="card-body">
78+
<h2 className="card-title">
79+
<Markdown content={contribution.title} />
80+
</h2>
81+
<span className="flex-1" />
82+
<span className="card-normal">{contribution.repository.project.name}</span>
83+
<span className="card-normal">
84+
{contribution.repository.owner}/{contribution.repository.name}
85+
</span>
86+
<div className="card-actions justify-end mt-4 gap-4">
87+
<img
88+
className="w-6 h-6 rounded-full"
89+
src={contribution.contributor.avatarUrl}
90+
/>
91+
<div className="flex-1" />
92+
{contribution.activityCount > 0 && (
93+
<div className="flex flex-row">
94+
<svg
95+
xmlns="http://www.w3.org/2000/svg"
96+
fill="none"
97+
viewBox="0 0 24 24"
98+
strokeWidth={1.5}
99+
stroke="currentColor"
100+
className="size-6"
101+
>
102+
<path
103+
strokeLinecap="round"
104+
strokeLinejoin="round"
105+
d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
106+
/>
107+
</svg>
108+
<span className="">{contribution.activityCount}</span>
109+
</div>
110+
)}
111+
<div className="flex flex-row">
112+
{getElapsedTime(contribution.updatedAt, localize("elapsed-time-suffixes"))}
113+
</div>
114+
<Link href={contribution.url} className="link">
115+
{contribution.type === "ISSUE"
116+
? localize("contribute-read-issue")
117+
: localize("contribute-review-changes")}
118+
</Link>
119+
</div>
120+
</div>
121+
</div>
122+
</div>
123+
</div>
124+
)}
125+
</div>
126+
</main>
127+
);
128+
}

web/src/pages/contribute/index.tsx

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { fetchContributionsListAction } from "src/redux/actions/contributions";
99
import { useAppDispatch, useAppSelector } from "src/redux/store";
1010
import { getElapsedTime } from "src/utils/elapsed-time";
1111
import React from "react";
12+
import { getContributionURL } from "src/utils/contribution";
1213

1314
// ts-prune-ignore-next
1415
export default function Page(): JSX.Element {
@@ -50,52 +51,52 @@ export default function Page(): JSX.Element {
5051
className="card card-compact bg-base-300 flex-auto w-full max-w-xs sm:max-w-sm"
5152
key={contributionIndex}
5253
>
53-
<div className="card-body markdown">
54-
<div className="card-body">
55-
<h2 className="card-title">
56-
<Markdown content={contribution.title} />
57-
</h2>
58-
<span className="flex-1" />
59-
<span className="card-normal">{contribution.repository.project.name}</span>
60-
<span className="card-normal">
61-
{contribution.repository.owner}/{contribution.repository.name}
62-
</span>
63-
<div className="card-actions justify-end mt-4 gap-4">
64-
<img
65-
className="w-6 h-6 rounded-full"
66-
src={contribution.contributor.avatarUrl}
67-
/>
68-
<div className="flex-1" />
69-
{contribution.activityCount > 0 && (
54+
<Link href={getContributionURL(contribution)}>
55+
<div className="card-body markdown">
56+
<div className="card-body">
57+
<h2 className="card-title">
58+
<Markdown content={contribution.title} />
59+
</h2>
60+
<span className="flex-1" />
61+
<span className="card-normal">{contribution.repository.project.name}</span>
62+
<span className="card-normal">
63+
{contribution.repository.owner}/{contribution.repository.name}
64+
</span>
65+
<div className="card-actions justify-end mt-4 gap-4">
66+
<img
67+
className="w-6 h-6 rounded-full"
68+
src={contribution.contributor.avatarUrl}
69+
/>
70+
<div className="flex-1" />
71+
{contribution.activityCount > 0 && (
72+
<div className="flex flex-row">
73+
<svg
74+
xmlns="http://www.w3.org/2000/svg"
75+
fill="none"
76+
viewBox="0 0 24 24"
77+
strokeWidth={1.5}
78+
stroke="currentColor"
79+
className="size-6"
80+
>
81+
<path
82+
strokeLinecap="round"
83+
strokeLinejoin="round"
84+
d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
85+
/>
86+
</svg>
87+
<span className="">{contribution.activityCount}</span>
88+
</div>
89+
)}
7090
<div className="flex flex-row">
71-
<svg
72-
xmlns="http://www.w3.org/2000/svg"
73-
fill="none"
74-
viewBox="0 0 24 24"
75-
strokeWidth={1.5}
76-
stroke="currentColor"
77-
className="size-6"
78-
>
79-
<path
80-
strokeLinecap="round"
81-
strokeLinejoin="round"
82-
d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
83-
/>
84-
</svg>
85-
<span className="">{contribution.activityCount}</span>
91+
{getElapsedTime(
92+
contribution.updatedAt,
93+
localize("elapsed-time-suffixes"),
94+
)}
8695
</div>
87-
)}
88-
<div className="flex flex-row">
89-
{getElapsedTime(contribution.updatedAt, localize("elapsed-time-suffixes"))}
9096
</div>
91-
<Link href={contribution.url} className="link">
92-
{contribution.type === "ISSUE"
93-
? localize("contribute-read-issue")
94-
: localize("contribute-review-changes")}
95-
</Link>
9697
</div>
9798
</div>
98-
</div>
99+
</Link>
99100
</div>
100101
))}
101102
</div>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Action, ThunkAction } from "@reduxjs/toolkit";
2+
import { captureException } from "@sentry/react";
3+
import { contributionPageSlice } from "src/redux/slices/contribution-page";
4+
import { AppState } from "src/redux/store";
5+
import { fetchV2 } from "src/utils/fetch";
6+
7+
export const fetchContributionAction =
8+
(id?: string): ThunkAction<void, AppState, unknown, Action> =>
9+
async (dispatch) => {
10+
if (!id) {
11+
dispatch(contributionPageSlice.actions.set({ contribution: "404" }));
12+
return;
13+
}
14+
try {
15+
dispatch(contributionPageSlice.actions.set({ contribution: null }));
16+
const { contribution } = await fetchV2("api:Contributions/:id", { params: { id } });
17+
dispatch(contributionPageSlice.actions.set({ contribution }));
18+
} catch (error) {
19+
dispatch(contributionPageSlice.actions.set({ contribution: "ERROR" }));
20+
captureException(error, { tags: { type: "WEB_FETCH" } });
21+
}
22+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { GetContributionResponse } from "@dzcode.io/api/dist/contribution/types";
2+
import { createSlice } from "@reduxjs/toolkit";
3+
import { setReducerFactory } from "src/redux/utils";
4+
import { Loadable } from "src/utils/loadable";
5+
6+
// ts-prune-ignore-next
7+
export interface ContributionPageState {
8+
contribution: Loadable<GetContributionResponse["contribution"], "404">;
9+
}
10+
11+
const initialState: ContributionPageState = {
12+
contribution: null,
13+
};
14+
15+
export const contributionPageSlice = createSlice({
16+
name: "contribution-page",
17+
initialState,
18+
reducers: {
19+
set: setReducerFactory(),
20+
},
21+
});

web/src/redux/store.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { PropsWithChildren, useState } from "react";
44
import { Provider as ReduxProvider, useDispatch, useSelector } from "react-redux";
55

66
import { contributionsPageSlice } from "./slices/contributions-page";
7+
import { contributionPageSlice } from "./slices/contribution-page";
78
import { contributorsPageSlice } from "./slices/contributors-page";
89
import { landingPageSlice } from "./slices/landing-page";
910
import { projectsPageSlice } from "./slices/projects-page";
@@ -20,6 +21,7 @@ const makeAppStore = () => {
2021
contributorsPage: contributorsPageSlice.reducer,
2122
contributorPage: contributorPageSlice.reducer,
2223
contributionsPage: contributionsPageSlice.reducer,
24+
contributionPage: contributionPageSlice.reducer,
2325
landingPage: landingPageSlice.reducer,
2426
},
2527
});

0 commit comments

Comments
 (0)