Skip to content

Commit a37d2a2

Browse files
committed
add-profile-activities
1 parent 13dc461 commit a37d2a2

File tree

13 files changed

+239
-31
lines changed

13 files changed

+239
-31
lines changed

API/Controllers/ProfilesController.cs

+13-5
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44
namespace API.Controllers
55
{
66
public class ProfilesController : BaseApiController
7+
{
8+
[HttpGet("{username}")]
9+
public async Task<IActionResult> GetProfile(string username)
710
{
8-
[HttpGet("{username}")]
9-
public async Task<IActionResult> GetProfile(string username)
10-
{
11-
return HandleResult(await Mediator.Send(new Details.Query{Username = username}));
12-
}
11+
return HandleResult(await Mediator.Send(new Details.Query { Username = username }));
1312
}
13+
14+
[HttpGet("{username}/activities")]
15+
public async Task<IActionResult> GetUserActivities(string username,
16+
string predicate)
17+
{
18+
return HandleResult(await Mediator.Send(new ListActivities.Query
19+
{ Username = username, Predicate = predicate }));
20+
}
21+
}
1422
}

Application/Core/MappingProfiles.cs

+20-10
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
using Application.Activities;
22
using Application.Comments;
3-
using AutoMapper;
3+
using Application.Profiles;
44
using Domain;
55
using Persistence;
66

77
namespace Application.Core
88
{
9-
public class MappingProfiles : Profile
9+
public class MappingProfiles : AutoMapper.Profile
1010
{
1111
public MappingProfiles()
1212
{
@@ -23,24 +23,34 @@ public MappingProfiles()
2323
.ForMember(d => d.Bio, o => o.MapFrom(s => s.AppUser.Bio))
2424
.ForMember(d => d.Image, o =>
2525
o.MapFrom(s => s.AppUser.Photos.FirstOrDefault(x => x.IsMain).Url))
26-
.ForMember(d => d.FollowersCount, o => o.MapFrom(s => s.AppUser.Followers.Count))
27-
.ForMember(d => d.FollowingCount, o => o.MapFrom(s => s.AppUser.Followings.Count))
26+
.ForMember(d => d.FollowersCount, o => o.MapFrom(s => s.AppUser.Followers.Count))
27+
.ForMember(d => d.FollowingCount, o => o.MapFrom(s => s.AppUser.Followings.Count))
2828
.ForMember(d => d.Following, o => o.MapFrom(s => s.AppUser.Followers.Any(x =>
2929
x.Observer.UserName == currentUsername)));
3030

3131
CreateMap<AppUser, Profiles.Profile>()
32-
.ForMember(d => d.Image, o =>
32+
.ForMember(d => d.Image, o =>
3333
o.MapFrom(s => s.Photos.FirstOrDefault(x => x.IsMain).Url))
34-
.ForMember(d => d.FollowersCount, o => o.MapFrom(s => s.Followers.Count))
35-
.ForMember(d => d.FollowingCount, o => o.MapFrom(s => s.Followings.Count))
34+
.ForMember(d => d.FollowersCount, o => o.MapFrom(s => s.Followers.Count))
35+
.ForMember(d => d.FollowingCount, o => o.MapFrom(s => s.Followings.Count))
3636
.ForMember(d => d.Following, o => o.MapFrom(s => s.Followers.Any(x =>
3737
x.Observer.UserName == currentUsername)));
3838

3939
CreateMap<Comment, CommentDto>()
40-
.ForMember(d => d.DisplayName, o => o.MapFrom(s => s.Author.DisplayName))
41-
.ForMember(d => d.Username, o => o.MapFrom(s => s.Author.UserName))
42-
.ForMember(d => d.Image, o =>
40+
.ForMember(d => d.DisplayName, o => o.MapFrom(s => s.Author.DisplayName))
41+
.ForMember(d => d.Username, o => o.MapFrom(s => s.Author.UserName))
42+
.ForMember(d => d.Image, o =>
4343
o.MapFrom(s => s.Author.Photos.FirstOrDefault(x => x.IsMain).Url));
44+
45+
CreateMap<ActivityAttendee, UserActivityDto>()
46+
.ForMember(d => d.Id, o => o.MapFrom(s => s.Activity.Id))
47+
.ForMember(d => d.Date, o => o.MapFrom(s => s.Activity.Date))
48+
.ForMember(d => d.Title, o => o.MapFrom(s => s.Activity.Title))
49+
.ForMember(d => d.Category, o => o.MapFrom(s =>
50+
s.Activity.Category))
51+
.ForMember(d => d.HostUsername, o => o.MapFrom(s =>
52+
s.Activity.Attendees.FirstOrDefault(x =>
53+
x.IsHost).AppUser.UserName));
4454
}
4555
}
4656
}
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using Application.Core;
2+
using AutoMapper;
3+
using AutoMapper.QueryableExtensions;
4+
using MediatR;
5+
using Microsoft.EntityFrameworkCore;
6+
using Persistence;
7+
8+
namespace Application.Profiles
9+
{
10+
public class ListActivities
11+
{
12+
public class Query : IRequest<Result<List<UserActivityDto>>>
13+
{
14+
public string Username { get; set; }
15+
public string Predicate { get; set; }
16+
}
17+
public class Handler : IRequestHandler<Query,
18+
Result<List<UserActivityDto>>>
19+
{
20+
private readonly DataContext _context;
21+
private readonly IMapper _mapper;
22+
public Handler(DataContext context, IMapper mapper)
23+
{
24+
_mapper = mapper;
25+
_context = context;
26+
}
27+
28+
public async Task<Result<List<UserActivityDto>>> Handle(Query
29+
request, CancellationToken cancellationToken)
30+
{
31+
var query = _context.ActivityAttendees
32+
.Where(u => u.AppUser.UserName == request.Username)
33+
.OrderBy(a => a.Activity.Date)
34+
.ProjectTo<UserActivityDto>(_mapper.ConfigurationProvider)
35+
.AsQueryable();
36+
query = request.Predicate switch
37+
{
38+
"past" => query.Where(a => a.Date <= DateTime.Now),
39+
"hosting" => query.Where(a => a.HostUsername ==
40+
request.Username),
41+
_ => query.Where(a => a.Date >= DateTime.Now)
42+
};
43+
var activities = await query.ToListAsync();
44+
return Result<List<UserActivityDto>>.Success(activities);
45+
}
46+
}
47+
}
48+
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Application.Profiles
4+
{
5+
public class UserActivityDto
6+
{
7+
public Guid Id { get; set; }
8+
public string Title { get; set; }
9+
public string Category { get; set; }
10+
public DateTime Date { get; set; }
11+
[JsonIgnore]
12+
public string HostUsername { get; set; }
13+
}
14+
}

client-app/src/app/Models/Profile.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,11 @@ export interface Photo {
2323
id: string;
2424
url: string;
2525
isMain: boolean;
26-
}
26+
}
27+
28+
export interface UserActivity {
29+
id: string;
30+
title: string;
31+
category: string;
32+
date: Date;
33+
}

client-app/src/app/api/agent.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Activity, ActivityFormValues } from "../Models/activity";
44
import { User, UserFormValues } from "../Models/user";
55
import { router } from "../router/Router";
66
import { store } from "../stores/store";
7-
import { Photo, Profile } from "../Models/Profile";
7+
import { Photo, Profile, UserActivity } from "../Models/Profile";
88
import { PaginatedResult } from "../Models/pagination";
99

1010
const sleep = (dalay: number) => {
@@ -84,8 +84,10 @@ const requests = {
8484
};
8585

8686
const Activities = {
87-
list: (params: URLSearchParams) => axios.get<PaginatedResult<Activity[]>>("./activities", {params})
88-
.then(responseBody),
87+
list: (params: URLSearchParams) =>
88+
axios
89+
.get<PaginatedResult<Activity[]>>("./activities", { params })
90+
.then(responseBody),
8991
details: (id: string) => requests.get<Activity>(`/activities/${id}`),
9092
create: (activity: ActivityFormValues) =>
9193
requests.post<void>("/activities", activity),
@@ -117,6 +119,10 @@ const Profiles = {
117119
requests.post(`/follow/${username}`, {}),
118120
listFollowings: (username: string, predicate: string) =>
119121
requests.get<Profile[]>(`/follow/${username}?predicate=${predicate}`),
122+
listActivities: (username: string, predicate: string) =>
123+
requests.get<UserActivity[]>(
124+
`/profiles/${username}/activities?predicate=${predicate}`
125+
),
120126
};
121127

122128
const agent = {

client-app/src/app/layout/App.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Container } from "semantic-ui-react";
22
import Navbar from "./Navbar";
33
import { observer } from "mobx-react-lite";
4-
import { Outlet, useLocation } from "react-router-dom";
4+
import { Outlet, ScrollRestoration, useLocation } from "react-router-dom";
55
import HomePage from "../../features/home/HomePage";
66
import { ToastContainer } from "react-toastify";
77
import { useStore } from "../stores/store";
@@ -25,6 +25,7 @@ function App() {
2525

2626
return (
2727
<>
28+
<ScrollRestoration />
2829
<ModalContainer />
2930
<ToastContainer position="bottom-right" hideProgressBar theme="colored" />
3031
{location.pathname === "/" ? (
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Navigate, Outlet, useLocation } from "react-router-dom";
2+
import { useStore } from "../stores/store";
3+
4+
5+
const RequireAuth = () => {
6+
const {userStore: {isLoggedIn}} = useStore();
7+
const location = useLocation();
8+
9+
if(!isLoggedIn) {
10+
return <Navigate to='/' state={{from: location}}/>
11+
}
12+
13+
return <Outlet />
14+
}
15+
16+
export default RequireAuth

client-app/src/app/router/Router.tsx

+15-8
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,29 @@ import ActivityForm from "../../features/activities/form/ActivityForm";
66
import NotFound from "../../features/errors/NotFound";
77
import ServerError from "../../features/errors/ServerError";
88
import TestErrors from "../../features/errors/TestError";
9-
import LoginForm from "../../features/users/LoginForm";
109
import App from "../layout/App";
1110
import ProfilePage from "../../features/profiles/ProfilePage";
11+
import RequireAuth from "./RequireAuth";
1212

1313
export const routes: RouteObject[] = [
1414
{
1515
path: "/",
1616
element: <App />,
1717
children: [
18-
{ path: "activities", element: <ActivityDashboard /> },
19-
{ path: "activities/:id", element: <ActivityDetails /> },
20-
{ path: "createActivity", element: <ActivityForm key="create" /> },
21-
{ path: "manage/:id", element: <ActivityForm key="manage" /> },
22-
{ path: "profiles/:username", element: <ProfilePage key="profile" /> },
23-
{ path: "login", element: <LoginForm /> },
24-
{ path: "errors", element: <TestErrors /> },
18+
{
19+
element: <RequireAuth />,
20+
children: [
21+
{ path: "activities", element: <ActivityDashboard /> },
22+
{ path: "activities/:id", element: <ActivityDetails /> },
23+
{ path: "createActivity", element: <ActivityForm key="create" /> },
24+
{ path: "manage/:id", element: <ActivityForm key="manage" /> },
25+
{
26+
path: "profiles/:username",
27+
element: <ProfilePage key="profile" />,
28+
},
29+
{ path: "errors", element: <TestErrors /> },
30+
],
31+
},
2532
{ path: "not-found", element: <NotFound /> },
2633
{ path: "server-error", element: <ServerError /> },
2734
{ path: "*", element: <Navigate replace to="/not-found" /> },

client-app/src/app/stores/profileStore.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { makeAutoObservable, reaction, runInAction } from "mobx";
2-
import { Photo, Profile } from "../Models/Profile";
2+
import { Photo, Profile, UserActivity } from "../Models/Profile";
33
import agent from "../api/agent";
44
import { store } from "./store";
55

@@ -11,6 +11,8 @@ export default class ProfileStore {
1111
followings: Profile[] = [];
1212
loadingFollowings = false;
1313
activeTab = 0;
14+
userActivities: UserActivity[] = [];
15+
loadingActivities = false;
1416

1517
constructor() {
1618
makeAutoObservable(this);
@@ -171,4 +173,21 @@ export default class ProfileStore {
171173
runInAction(() => (this.loadingFollowings = false));
172174
}
173175
};
176+
177+
loadUserActivities = async (username: string, predicate?: string) => {
178+
this.loadingActivities = true;
179+
try {
180+
const activities = await agent.Profiles.listActivities(username,
181+
predicate!);
182+
runInAction(() => {
183+
this.userActivities = activities;
184+
this.loadingActivities = false;
185+
})
186+
} catch (error) {
187+
console.log(error);
188+
runInAction(() => {
189+
this.loadingActivities = false;
190+
})
191+
}
192+
}
174193
}

client-app/src/features/activities/dashboard/ActivityDashboard.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { observer } from "mobx-react-lite";
22
import { useEffect, useState } from "react";
33
import { Grid, Loader } from "semantic-ui-react";
4-
import LoadingComponent from "../../../app/layout/LoadingComponent";
54
import { useStore } from "../../../app/stores/store";
65
import ActivityFilters from "./ActivityFilters";
76
import ActivityList from "./ActivityList";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, { SyntheticEvent, useEffect } from "react";
2+
import { observer } from "mobx-react-lite";
3+
import { Tab, Grid, Header, Card, Image, TabProps } from "semantic-ui-react";
4+
import { Link } from "react-router-dom";
5+
// import { UserActivity } from '../../app/models/profile';
6+
import { format } from "date-fns";
7+
import { useStore } from "../../app/stores/store";
8+
import { UserActivity } from "../../app/Models/Profile";
9+
10+
const panes = [
11+
{ menuItem: "Future Events", pane: { key: "future" } },
12+
{ menuItem: "Past Events", pane: { key: "past" } },
13+
{ menuItem: "Hosting", pane: { key: "hosting" } },
14+
];
15+
16+
const ProfileActivities = () => {
17+
const { profileStore } = useStore();
18+
const { loadUserActivities, profile, loadingActivities, userActivities } =
19+
profileStore;
20+
21+
useEffect(() => {
22+
loadUserActivities(profile!.username);
23+
}, [loadUserActivities, profile]);
24+
25+
const handleTabChange = (e: SyntheticEvent, data: TabProps) => {
26+
loadUserActivities(
27+
profile!.username,
28+
panes[data.activeIndex as number].pane.key
29+
);
30+
};
31+
32+
return (
33+
<Tab.Pane loading={loadingActivities}>
34+
<Grid>
35+
<Grid.Column width={16}>
36+
<Header floated="left" icon="calendar" content={"Activities"} />
37+
</Grid.Column>
38+
<Grid.Column width={16}>
39+
<Tab
40+
panes={panes}
41+
menu={{ secondary: true, pointing: true }}
42+
onTabChange={(e, data) => handleTabChange(e, data)}
43+
/>
44+
<br />
45+
<Card.Group itemsPerRow={4}>
46+
{userActivities.map((activity: UserActivity) => (
47+
<Card
48+
as={Link}
49+
to={`/activities/${activity.id}`}
50+
key={activity.id}
51+
>
52+
<Image
53+
src={`/assets/categoryImages/${activity.category}.jpg`}
54+
style={{ minHeight: 100, objectFit: "cover" }}
55+
/>
56+
<Card.Content>
57+
<Card.Header textAlign="center">{activity.title}</Card.Header>
58+
<Card.Meta textAlign="center">
59+
<div>{format(new Date(activity.date), "do LLL")}</div>
60+
<div>{format(new Date(activity.date), "h:mm a")}</div>
61+
</Card.Meta>
62+
</Card.Content>
63+
</Card>
64+
))}
65+
</Card.Group>
66+
</Grid.Column>
67+
</Grid>
68+
</Tab.Pane>
69+
);
70+
};
71+
72+
export default observer(ProfileActivities);

0 commit comments

Comments
 (0)