Skip to content

Commit

Permalink
Merge pull request #10 from Intrepiware/add-workout-history
Browse files Browse the repository at this point in the history
Add workout history
  • Loading branch information
Intrepiware authored Dec 30, 2023
2 parents b345691 + eaf7f85 commit 6e131b0
Show file tree
Hide file tree
Showing 14 changed files with 271 additions and 12 deletions.
1 change: 1 addition & 0 deletions WorkoutBuilder/ClientApp/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
entry: {
"home-index": "./src/home-index.tsx",
"timing-index": "./src/timing-index.tsx",
"workouts-index": "./src/workouts-index.tsx",
site: "./src/site.ts",
},
output: {
Expand Down
2 changes: 1 addition & 1 deletion WorkoutBuilder/ClientApp/src/Pages/HomeIndex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ function HomeIndex() {

const setFavorite = () => {
if (!!workout) {
fetch(`/Home/Favorite/${workout.publicId}`, {
fetch(`/Workouts/Favorite/${workout.publicId}`, {
method: "POST",
credentials: "include",
})
Expand Down
127 changes: 127 additions & 0 deletions WorkoutBuilder/ClientApp/src/Pages/WorkoutsIndex.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { useEffect, useState } from "react";
import { WorkoutListItem, getUserWorkouts } from "../apis/workoutListItem";

interface QueryCriteria {
take: number;
skip: number;
favorites: boolean;
}

function WorkoutsIndex() {
const [query, setQuery] = useState<QueryCriteria>({
skip: 0,
take: 26,
favorites: false,
});
const [workouts, setWorkouts] = useState<WorkoutListItem[]>([]);

useEffect(() => {
getUserWorkouts(query.skip, query.take, query.favorites).then(
(data: WorkoutListItem[]) => setWorkouts(data)
);
}, [query]);

const setFavorite = (id: string) => {
if (!!id) {
fetch(`/Workouts/Favorite/${id}`, {
method: "POST",
credentials: "include",
})
.then((res) => res.json())
.then((json) => {
const workoutsClone = [...workouts];
const index = workoutsClone.findIndex((x) => x.publicId == id);
workoutsClone[index].isFavorite = json.isFavorite;
setWorkouts(workoutsClone);
});
}
};

const formatDate = (dateString: string) => {
const date = new Date(`${dateString}Z`);
const hoursSince =
(new Date().getTime() - date.getTime()) / (1000 * 60 * 60);
return hoursSince >= 24
? date.toLocaleDateString()
: date.toLocaleTimeString();
};

return (
<section className="section">
<div className="container is-max-desktop">
<div className="buttons has-addons">
<button
className={`button ${query.favorites ? "is-info" : ""}`}
onClick={() => setQuery((x) => ({ ...x, favorites: true }))}
>
Favorites
</button>
<button
className={`button ${query.favorites ? "" : "is-info"}`}
onClick={() => setQuery((x) => ({ ...x, favorites: false }))}
>
All
</button>
</div>
<div className="table-container">
<table className="table is-fullwidth">
<thead>
<tr>
<th>Name</th>
<th>Created</th>
<th className="has-text-centered">Favorite</th>
</tr>
</thead>
<tbody>
{workouts &&
workouts.slice(0, 25).map((x) => (
<tr key={x.id}>
<td>
<a href={`${document.location.origin}?id=${x.publicId}`}>
{x.name}
</a>
</td>
<td>{formatDate(x.createDate)}</td>
<td className="has-text-centered">
<a
className="button"
onClick={() => setFavorite(x.publicId)}
>
<span className="icon is-small">
<span
className={`material-symbols-outlined ${
x.isFavorite ? "filled-star" : ""
}`}
>
star
</span>
</span>
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="buttons is-right">
<button
className="button is-info"
disabled={query.skip == 0}
onClick={() => setQuery((x) => ({ ...x, skip: x.skip - 25 }))}
>
Prev
</button>
<button
className="button is-info"
disabled={workouts.length < 26}
onClick={() => setQuery((x) => ({ ...x, skip: x.skip + 25 }))}
>
Next
</button>
</div>
</div>
</section>
);
}

export default WorkoutsIndex;
21 changes: 21 additions & 0 deletions WorkoutBuilder/ClientApp/src/apis/workoutListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface WorkoutListItem {
id: number;
publicId: string;
createDate: string;
isFavorite: boolean;
name: string;
}

export function getUserWorkouts(
skip: number,
take: number,
favorites: boolean
): Promise<WorkoutListItem[]> {
const takeParam = `?take=${take}`;
const skipParam = skip > 0 ? `&skip=${skip}` : "";
const favoritesParam = favorites ? "&onlyFavorites=true" : "";
return fetch(`/Workouts${takeParam}${skipParam}${favoritesParam}`, {
credentials: "include",
headers: { "X-Requested-With": "XMLHttpRequest" },
}).then((res) => res.json());
}
9 changes: 9 additions & 0 deletions WorkoutBuilder/ClientApp/src/workouts-index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from "react";
import ReactDOM from "react-dom/client";
import WorkoutsIndex from "./Pages/WorkoutsIndex";

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<WorkoutsIndex />
</React.StrictMode>
);
11 changes: 0 additions & 11 deletions WorkoutBuilder/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ public class HomeController : Controller
public IEmailService EmailService { protected get; init; } = null!;
public IConfiguration Configuration { protected get; init; } = null!;
public IHomeWorkoutModelMapper HomeWorkoutModelMapper { protected get; init; } = null!;
public IWorkoutService WorkoutService { protected get; init; } = null!;
public IUrlBuilder UrlBuilder { protected get; init; } = null!;

public IActionResult Index()
{
Expand Down Expand Up @@ -93,15 +91,6 @@ public IActionResult Contact(HomeContactRequestModel data)
return View(new HomeContactRequestModel());
}

[HttpPost]
public async Task<IActionResult> Favorite(string id)
{
var workout = await WorkoutService.ToggleFavorite(id);
if (workout != null && workout.PublicId != id)
Response.Headers.Add("Location", UrlBuilder.Action("Index", "Home", new { id = workout.PublicId }));
return Json(new { success = true, workout.IsFavorite });
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
Expand Down
57 changes: 57 additions & 0 deletions WorkoutBuilder/Controllers/WorkoutsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using WorkoutBuilder.Data;
using WorkoutBuilder.Models;
using WorkoutBuilder.Services;
using WorkoutBuilder.Services.ExtensionMethods;
using WorkoutBuilder.Services.Impl;
using WorkoutBuilder.Services.Models;

namespace WorkoutBuilder.Controllers
{
[Authorize]
public class WorkoutsController : Controller
{
public IRepository<Workout> WorkoutRepository { init; protected get; } = null!;
public IUserContext UserContext { init; protected get; } = null!;
public IWorkoutService WorkoutService { protected get; init; } = null!;
public IUrlBuilder UrlBuilder { protected get; init; } = null!;

public IActionResult Index(int take = 25, int skip = 0, bool onlyFavorites = false)
{
if(Request.IsAjaxRequest())
{
var query = WorkoutRepository.GetAll().Where(x => x.UserId == UserContext.GetUserId().Value);

if (onlyFavorites)
query = query.Where(x => x.IsFavorite);

var output = query
.OrderByDescending(x => x.CreateDate)
.Skip(skip).Take(take)
.Select(x => new WorkoutListItemModel
{
CreateDate = x.CreateDate,
Id = x.Id,
IsFavorite = x.IsFavorite,
Name = JsonConvert.DeserializeObject<WorkoutGenerationResponseModel>(x.Body).Name,
PublicId = x.PublicId
})
.ToList();

return Json(output);
}
return View();
}

[HttpPost]
public async Task<IActionResult> Favorite(string id)
{
var workout = await WorkoutService.ToggleFavorite(id);
if (workout != null && workout.PublicId != id)
Response.Headers.Add("Location", UrlBuilder.Action("Index", "Home", new { id = workout.PublicId }));
return Json(new { success = true, workout.IsFavorite });
}
}
}
1 change: 1 addition & 0 deletions WorkoutBuilder/IOC/AutofacRegistrationModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ protected override void Load(ContainerBuilder builder)
// Controllers
builder.RegisterType<HomeController>().PropertiesAutowired();
builder.RegisterType<UsersController>().PropertiesAutowired();
builder.RegisterType<WorkoutsController>().PropertiesAutowired();

// View Components
builder.RegisterType<TopMenuViewComponent>().PropertiesAutowired();
Expand Down
11 changes: 11 additions & 0 deletions WorkoutBuilder/Models/ListItems/WorkoutListItemModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace WorkoutBuilder.Models
{
public class WorkoutListItemModel
{
public long Id { get; set; }
public string PublicId { get; set; }
public DateTime CreateDate { get; set; }
public bool IsFavorite { get; set; }
public string Name { get; set; }
}
}
1 change: 1 addition & 0 deletions WorkoutBuilder/Models/TopMenuModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public class TopMenuModel
public MenuItemModel? Logout { get; set; }
public MenuItemModel? Login { get; set; }
public MenuItemModel? SignUp { get; set; }
public MenuItemModel? Workouts { get; set; }
}

public class MenuItemModel
Expand Down
23 changes: 23 additions & 0 deletions WorkoutBuilder/Services/ExtensionMethods/HttpRequestExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace WorkoutBuilder.Services.ExtensionMethods
{
public static class HttpRequestExtensions
{
/// <summary>
/// Determines whether the specified HTTP request is an AJAX request.
/// </summary>
///
/// <returns>
/// true if the specified HTTP request is an AJAX request; otherwise, false.
/// </returns>
/// <param name="request">The HTTP request.</param><exception cref="T:System.ArgumentNullException">The <paramref name="request"/> parameter is null (Nothing in Visual Basic).</exception>
public static bool IsAjaxRequest(this HttpRequest request)
{
if (request == null)
throw new ArgumentNullException(nameof(request));

if (request.Headers != null)
return request.Headers["X-Requested-With"] == "XMLHttpRequest";
return false;
}
}
}
3 changes: 3 additions & 0 deletions WorkoutBuilder/ViewComponents/TopMenuViewComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ public Task<IViewComponentResult> InvokeAsync()
model.Contact = new MenuItemModel { DisplayName = "Contact", Url = UrlBuilder.Action("Contact", "Home", null) };
model.TimingCalc = new MenuItemModel { DisplayName = "Timing Calc", Url = UrlBuilder.Action("Index", "Timing", null) };
if (UserContext.GetUserId() != null)
{
model.Logout = new MenuItemModel { DisplayName = "Logout", Url = UrlBuilder.Action("Logout", "Users", null) };
model.Workouts = new MenuItemModel { DisplayName = "Workouts", Url = UrlBuilder.Action("Index", "Workouts", null) };
}
else
model.Login = new MenuItemModel { DisplayName = "Login", Url = UrlBuilder.Action("Login", "Users", null) };

Expand Down
6 changes: 6 additions & 0 deletions WorkoutBuilder/Views/Shared/Components/TopMenu/TopMenu.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
<a class="navbar-item" href="@Model.Contact.Url">
@Model.Contact.DisplayName
</a>
@if(Model.Workouts != null)
{
<a class="navbar-item" href="@Model.Workouts.Url">
@Model.Workouts.DisplayName
</a>
}
</div>

<div class="navbar-end">
Expand Down
10 changes: 10 additions & 0 deletions WorkoutBuilder/Views/Workouts/Index.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@{
ViewData["Title"] = "Workout Builder";
}

<div id="root"></div>

@section Scripts
{
<script src="~/js/pages/workouts-index.js"></script>
}

0 comments on commit 6e131b0

Please sign in to comment.