feat(user): add UserDetailsView to list (current)user (private-and-)public repositories@@ -1,5 +1,5 @@
{
- "_generatedAtUnix": 1663791331427,
+ "_generatedAtUnix": 1663803734312,
"_hashAlgorithm": "sha1",
"_version": 2,
"islands": {
@@ -43,10 +43,6 @@
"hash": "389861a6e7f9ff12026ddf5e91bdf4f06646116e",
"pathSource": "./app/views/InternalErrorView.tsx"
},
- "DashboardView": {
- "hash": "316530648e98734fd1e983c8dcf9062d772363fb",
- "pathSource": "./app/views/auth/DashboardView.tsx"
- },
"LoginView": {
"hash": "6d69b3db6e0c92ebcc9d7611a0275e06dcd18379",
"pathSource": "./app/views/auth/LoginView.tsx"
@@ -78,6 +74,14 @@
"RepositoryExploreView": {
"hash": "416d8fe270478274dc12f6ac09e75bb789842ec3",
"pathSource": "./app/views/repository/RepositoryExploreView.tsx"
+ },
+ "UserDashboardView": {
+ "hash": "142f85b0c41050a27b52a6356b9739c6f67824b3",
+ "pathSource": "./app/views/user/UserDashboardView.tsx"
+ },
+ "UserDetailsView": {
+ "hash": "af2b33f9d15192719e8d32339d82d0e893660a69",
+ "pathSource": "./app/views/user/UserDetailsView.tsx"
}
}
}
@@ -20,6 +20,9 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
if (commonProps.authenticated) {
return (
<>
+ <a href={`/@${commonProps.currentUserUsername || "ghost"}`}>
+ {commonProps.currentUserUsername || "ghost"}
+ </a>
<a aria-label={"Log off your account"} href={"/auth/logout"}>
Logout
</a>
@@ -1,4 +1,3 @@
-import { default as getDashboardView } from "./getDashboardView";
import { default as getLoginView } from "./getLoginView";
import { default as getLogoutAction } from "./getLogoutAction";
import { default as getRegisterView } from "./getRegisterView";
@@ -6,7 +5,6 @@ import { default as postLoginAction } from "./postLoginAction";
import { default as postRegisterAction } from "./postRegisterAction";
export const AuthController = {
- getDashboardView,
getLoginView,
getLogoutAction,
getRegisterView,
@@ -4,26 +4,33 @@ import type { ReqHandler } from "@ethicdevs/react-monolith";
import { AppRoute } from "../../routes";
import { makeUsersService } from "../../services/user";
// app views
-import DashboardView, {
- DashboardViewProps,
-} from "../../views/auth/DashboardView";
+import UserDashboardView, {
+ UserDashboardViewProps,
+} from "../../views/user/UserDashboardView";
-const getDashboardView: ReqHandler = async (request, reply) => {
+const getUserDashboardView: ReqHandler = async (request, reply) => {
const { authenticated, curr_user_uid } = request.session.data;
+
if (authenticated === false || curr_user_uid == null) {
- reply.redirect(307, request.namedViewsPathMap[AppRoute.AUTH_LOGIN]);
+ reply.redirect(302, request.namedViewsPathMap[AppRoute.AUTH_LOGIN]);
return reply;
}
const usersService = makeUsersService({ request });
const currentUser = await usersService.getUserById(curr_user_uid);
- const repositories = await usersService.getUserRepositories(curr_user_uid);
+
+ if (currentUser == null) {
+ reply.redirect(302, request.namedViewsPathMap[AppRoute.AUTH_LOGIN]);
+ return reply;
+ }
+
+ const repositories = await usersService.getUserRepositories(currentUser);
const reqHandler = reply.makeRequestHandler(request, reply);
- return reqHandler<DashboardViewProps>(DashboardView.name, {
+ return reqHandler<UserDashboardViewProps>(UserDashboardView.name, {
currentUser,
repositories,
});
};
-export default getDashboardView;
+export default getUserDashboardView;
@@ -0,0 +1,39 @@
+// 3rd-party
+import type { ReqHandler } from "@ethicdevs/react-monolith";
+import { User } from "@prisma/client";
+// app
+import { AppRoute, AppRoutesParams } from "../../routes";
+import { makeUsersService } from "../../services/user";
+// app views
+import UserDetailsView, {
+ UserDetailsViewProps,
+} from "../../views/user/UserDetailsView";
+
+const getUserDetailsView: ReqHandler = async (request, reply) => {
+ const { username } =
+ request.params as AppRoutesParams[AppRoute.USER_DETAILS]["params"];
+
+ const { curr_user_uid } = request.session.data;
+
+ const usersService = makeUsersService({ request });
+ const user = await usersService.getUserByUsername(username);
+
+ if (user == null) {
+ return reply.status(404).callNotFound();
+ }
+
+ let currentUser: User | null = null;
+
+ if (curr_user_uid != null && user.id === curr_user_uid) {
+ currentUser = await usersService.getUserById(curr_user_uid);
+ }
+
+ const reqHandler = reply.makeRequestHandler(request, reply);
+ return reqHandler<UserDetailsViewProps>(UserDetailsView.name, {
+ currentUser,
+ repositories: await usersService.getUserRepositories(user),
+ user,
+ });
+};
+
+export default getUserDetailsView;
@@ -0,0 +1,7 @@
+import { default as getUserDashboardView } from "./getUserDashboardView";
+import { default as getUserDetailsView } from "./getUserDetailsView";
+
+export const UserController = {
+ getUserDashboardView,
+ getUserDetailsView,
+};
@@ -12,6 +12,7 @@ import { authenticatedOrLogin, guestOrRedirect } from "./utils/server";
import { AuthController } from "./controllers/auth";
import { OrganizationController } from "./controllers/organization";
import { RepositoryController } from "./controllers/repository";
+import { UserController } from "./controllers/user";
import * as HomeController from "./controllers/HomeController";
import * as ThemeController from "./controllers/ThemeController";
@@ -24,6 +25,7 @@ export enum AppRoute {
AUTH_LOGIN_ACTION = "auth.login.action",
AUTH_LOGOUT_ACTION = "auth.logout.action",
USER_DASHBOARD = "user.dashboard",
+ USER_DETAILS = "user.details",
ORGANIZATION_DETAILS = "organization.details",
REPOSITORY_EXPLORE = "repository.explore",
REPOSITORY_COMMITS_LOG = "repository.commits_log",
@@ -55,6 +57,11 @@ export interface AppRoutesParams extends IRouteParams {
};
[AppRoute.AUTH_LOGOUT_ACTION]: undefined;
[AppRoute.USER_DASHBOARD]: undefined;
+ [AppRoute.USER_DETAILS]: {
+ params: {
+ username: string;
+ };
+ };
[AppRoute.ORGANIZATION_DETAILS]: {
params: {
orgSlug: string;
@@ -141,6 +148,18 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
},
[AppRoute.AUTH_LOGOUT_ACTION]: undefined,
[AppRoute.USER_DASHBOARD]: undefined,
+ [AppRoute.USER_DETAILS]: {
+ params: {
+ type: "object",
+ required: ["username"],
+ additionalProperties: false,
+ properties: {
+ username: {
+ type: "string",
+ },
+ },
+ },
+ },
[AppRoute.ORGANIZATION_DETAILS]: {
params: {
type: "object",
@@ -340,7 +359,13 @@ const RootAppRouter: AppRouter = () => {
method={"GET"}
path={"/dashboard"}
preHandler={loggedOrLoginRedirect}
- handler={AuthController.getDashboardView}
+ handler={UserController.getUserDashboardView}
+ />
+ <Router.Route
+ name={AppRoute.USER_DETAILS}
+ method={"GET"}
+ path={"/@:username"}
+ handler={UserController.getUserDetailsView}
/>
{/* --- */}
<Router.Route
@@ -51,27 +51,33 @@ const makeGetRepositoryCommitLog: ServiceMethodFactory<
cwd: repoPath,
});
- const gitLogResult = await new Promise<RepositoryLog[]>(
- (resolve, reject) => {
- let buffer = [] as string[];
- gitLogProcess.stdout.on("data", (data) => buffer.push(data));
- gitLogProcess.stderr.on("data", (data) => {
- reject(new Error(Buffer.from(data).toString("utf-8")));
- });
- gitLogProcess.stdout.on("close", () => {
- const escapedJson = buffer
- .join("")
- .replace(/\n\^@\^/g, "\\n^@^") // Escape unterminated lines: \n^@\^ -> \\n^@\^
- .replace(/"/gm, '\\"') // Escape double-quotes: " -> \"
- .replace(/\^@\^/gm, '"'); // Escaped double-quotes back to double quotes
- resolve(
- JSON.parse(`[${escapedJson.substring(0, escapedJson.length - 1)}]`)
- );
- });
- }
- );
+ try {
+ const gitLogResult = await new Promise<RepositoryLog[]>(
+ (resolve, reject) => {
+ let buffer = [] as string[];
+ gitLogProcess.stdout.on("data", (data) => buffer.push(data));
+ gitLogProcess.stderr.on("data", (data) => {
+ reject(new Error(Buffer.from(data).toString("utf-8")));
+ });
+ gitLogProcess.stdout.on("close", () => {
+ const escapedJson = buffer
+ .join("")
+ .replace(/\n\^@\^/g, "\\n^@^") // Escape unterminated lines: \n^@\^ -> \\n^@\^
+ .replace(/"/gm, '\\"') // Escape double-quotes: " -> \"
+ .replace(/\^@\^/gm, '"'); // Escaped double-quotes back to double quotes
+ resolve(
+ JSON.parse(
+ `[${escapedJson.substring(0, escapedJson.length - 1)}]`
+ )
+ );
+ });
+ }
+ );
- return gitLogResult as RepositoryLog[];
+ return gitLogResult as RepositoryLog[];
+ } catch (err) {
+ return [];
+ }
};
};
@@ -1,32 +1,32 @@
// 1st-party
import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
// generated via script[generate:prisma]
-import { Organization, Repository } from "@prisma/client";
+import { Prisma, Organization, Repository, User } from "@prisma/client";
// app
import type { UsersServiceDeps } from "./types";
const getUserRepositories: ServiceMethodFactory<
UsersServiceDeps,
- [string],
+ [User, undefined | Prisma.RepositoryWhereInput],
Promise<(Repository & { parentOrg: Organization })[]>
> = ({ request }) => {
- return async (userId) => {
+ return async (user, where = undefined) => {
const userRepos = await request.prisma.repository.findMany({
include: {
organization: true,
},
- where: {
+ where: where || {
OR: [
{
organization: {
- ownerId: userId,
+ ownerId: user.id,
},
},
{
organization: {
memberships: {
some: {
- userId,
+ userId: user.id,
},
},
},
@@ -4,6 +4,7 @@ import { ServiceApiContract } from "@ethicdevs/react-monolith";
import { FastifyRequest } from "fastify";
// generated via script[generate:prisma]
import {
+ Prisma,
Organization,
OrganizationMembership,
Repository,
@@ -19,7 +20,8 @@ export interface UsersServiceAPI extends ServiceApiContract {
): Promise<OrganizationMembership[]>;
getUserOrganizations(userId: string): Promise<Organization[]>;
getUserRepositories(
- userId: string
+ user: User,
+ where?: Prisma.RepositoryWhereInput
): Promise<(Repository & { parentOrg: Organization })[]>;
}
@@ -1,4 +1,4 @@
-import type { Prisma, GlobalRole } from "@prisma/client";
+import type { Prisma, GlobalRole, User } from "@prisma/client";
export type AppThemeScheme = "light" | "dark";
export type WithThemeSchemeProp = {
@@ -20,6 +20,11 @@ export interface AppSessionData extends Prisma.JsonObject {
export interface CommonViewProps {
authenticated: boolean;
+ currentUserAvatarUri: string | null;
+ currentUserId: string | null;
+ currentUserRole: GlobalRole | null;
+ currentUserUsername: string | null;
+ flashMessage: string | null;
themeScheme: AppThemeScheme;
title?: string;
}
@@ -13,16 +13,38 @@ export const makeRequestHandler = {
props?: T & CommonViewProps,
viewCtx?: ViewContext
) => {
+ const {
+ authenticated,
+ curr_user_uid,
+ curr_user_avatar_uri,
+ curr_user_role,
+ curr_user_username,
+ flash_message,
+ } = request.session.data;
+
+ const title = props != null ? props.title : "";
+
+ const themeSchemeFromCookies =
+ "theme_scheme" in request.cookies
+ ? request.cookies["theme_scheme"].split(".")[0]
+ : null;
+
+ const themeScheme =
+ (themeSchemeFromCookies || Const.DEFAULT_THEME_SCHEME) === "light"
+ ? "light"
+ : "dark";
+
const viewProps: T & { commonProps: CommonViewProps } = {
...props,
commonProps: {
- authenticated: request.session.data.authenticated,
- title: props?.title,
- themeScheme:
- (request.cookies?.["theme_scheme"]?.split(".")?.[0] ||
- Const.DEFAULT_THEME_SCHEME) === "light"
- ? "light"
- : "dark",
+ authenticated,
+ currentUserAvatarUri: curr_user_avatar_uri,
+ currentUserId: curr_user_uid,
+ currentUserRole: curr_user_role,
+ currentUserUsername: curr_user_username,
+ flashMessage: flash_message,
+ title,
+ themeScheme,
},
} as T & { commonProps: CommonViewProps };
@@ -10,12 +10,12 @@ import { Layout, PageWrapper } from "../../components";
// app islands
import RepositoriesList from "../../islands/RepositoriesList";
-export interface DashboardViewProps extends CommonProps {
+export interface UserDashboardViewProps extends CommonProps {
currentUser: User;
repositories: (Repository & { parentOrg: Organization })[];
}
-const DashboardView: ReactView<DashboardViewProps> = ({
+const UserDashboardView: ReactView<UserDashboardViewProps> = ({
commonProps,
currentUser,
repositories,
@@ -36,5 +36,5 @@ const DashboardView: ReactView<DashboardViewProps> = ({
);
};
-DashboardView.displayName = "DashboardView";
-export default DashboardView;
+UserDashboardView.displayName = "UserDashboardView";
+export default UserDashboardView;
@@ -0,0 +1,46 @@
+// 1st-party
+import type { ReactView } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React from "react";
+// generated via script[generate:prisma]
+import type { Organization, Repository, User } from "@prisma/client";
+// app
+import type { CommonProps } from "../../types";
+import { Layout, PageWrapper } from "../../components";
+// app islands
+import RepositoriesList from "../../islands/RepositoriesList";
+
+export interface UserDetailsViewProps extends CommonProps {
+ user: User;
+ currentUser: User | null;
+ repositories: (Repository & { parentOrg: Organization })[];
+}
+
+const UserDetailsView: ReactView<UserDetailsViewProps> = ({
+ commonProps,
+ currentUser,
+ user,
+ repositories,
+}) => {
+ return (
+ <Layout {...commonProps}>
+ <PageWrapper>
+ <h1>{user.displayName || user.username}</h1>
+ <h2>
+ {currentUser != null && currentUser.id === user.id
+ ? "Your repositories"
+ : "Public repositories from this user"}
+ </h2>
+ <div
+ data-islandid={`${RepositoriesList.name}$$0`}
+ style={{ width: "100%" }}
+ >
+ <RepositoriesList repositories={repositories} />
+ </div>
+ </PageWrapper>
+ </Layout>
+ );
+};
+
+UserDetailsView.displayName = "UserDetailsView";
+export default UserDetailsView;