diff --git a/backend/Controllers/GamesController.cs b/backend/Controllers/GamesController.cs index a492fb6..ad2d30b 100644 --- a/backend/Controllers/GamesController.cs +++ b/backend/Controllers/GamesController.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Threading.Tasks; using IP5.Model; +using IP5.Repositories; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -10,21 +12,37 @@ namespace IP5.Controllers public class GamesController : ControllerBase { private readonly ILogger _logger; + private readonly IGamesRepository _gamesRepository; - public GamesController(ILogger logger) + public GamesController(IGamesRepository gamesRepository, ILogger logger) { _logger = logger; + _gamesRepository = gamesRepository; } [HttpGet] - public IEnumerable Get() + public async IAsyncEnumerable GetAll() { - return new[] { - new Game {Player1 = "Joel", Player2 = "Rohn", Score1 = 3, Score2 = 6, Ongoing = true}, - new Game {Player1 = "Jan", Player2 = "Bram", Score1 = 5, Score2 = 11}, - new Game {Player1 = "Andy", Player2 = "Raul", Score1 = 11, Score2 = 9}, - new Game {Player1 = "Danny", Player2 = "Tony", Score1 = 11, Score2 = 9}, - }; + await foreach (var game in _gamesRepository.GetAll()) + yield return game; + } + + [HttpGet("{code}")] + public Task Get([FromRoute] string code) + { + return _gamesRepository.Get(code); + } + + [HttpPut] + public void Create(Game game) + { + _gamesRepository.Add(game); + } + + [HttpDelete("{code}")] + public void Delete(string code) + { + _gamesRepository.Delete(code); } } } \ No newline at end of file diff --git a/backend/Model/DbGame.cs b/backend/Model/DbGame.cs new file mode 100644 index 0000000..803de53 --- /dev/null +++ b/backend/Model/DbGame.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IP5.Repositories +{ + public class DbGame + { + public Guid Id { get; set; } + + [Column("player_one_id")] + public Guid PlayerOneId { get; set; } + + [Column("player_two_id")] + public Guid PlayerTwoId { get; set; } + + [Column("score_one")] + public int ScoreOne { get; set; } + + [Column("score_two")] + public int ScoreTwo { get; set; } + + [Column("is_active")] + public bool IsActive { get; set; } + } +} diff --git a/backend/ModelView/Game.cs b/backend/ModelView/Game.cs index 285c652..51e5fc6 100644 --- a/backend/ModelView/Game.cs +++ b/backend/ModelView/Game.cs @@ -2,10 +2,11 @@ namespace IP5.Model { public class Game { - public string Player1 { get; set; } - public string Player2 { get; set; } - public int Score1 { get; set; } - public int Score2 { get; set; } - public bool Ongoing { get; set; } + public string Code { get; set; } + public string PlayerOneCode { get; set; } + public string PlayerTwoCode { get; set; } + public int ScoreOne { get; set; } + public int ScoreTwo { get; set; } + public bool IsActive { get; set; } } } \ No newline at end of file diff --git a/backend/Repositories/GamesRepository.cs b/backend/Repositories/GamesRepository.cs new file mode 100644 index 0000000..c50d95c --- /dev/null +++ b/backend/Repositories/GamesRepository.cs @@ -0,0 +1,72 @@ +using IP5.Extensions; +using IP5.Model; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace IP5.Repositories +{ + public interface IGamesRepository + { + IAsyncEnumerable GetAll(); + Task Add(Game game); + Task Delete(string code); + Task Get(string code); + } + + public class GamesRepository : IGamesRepository + { + private PingPongContext _db; + + public GamesRepository(PingPongContext db) + { + _db = db; + } + + public Task Add(Game game) + { + _db.Games.Add(new DbGame { + PlayerOneId = game.PlayerOneCode.ToGuid(), + PlayerTwoId = game.PlayerTwoCode.ToGuid() + }); + + return _db.SaveChangesAsync(); + } + + public Task Delete(string code) + { + var game = _db.Games.Where(i => i.Id == code.ToGuid()); + _db.Remove(game); + return _db.SaveChangesAsync(); + } + + public Task Get(string code) + { + return _db.Games.Where(i => i.Id == code.ToGuid()) + .Select(i => new Game + { + Code = i.Id.ToBase64(), + IsActive = i.IsActive, + PlayerOneCode = i.PlayerOneId.ToBase64(), + PlayerTwoCode = i.PlayerTwoId.ToBase64(), + ScoreOne = i.ScoreOne, + ScoreTwo = i.ScoreTwo + }).FirstAsync(); + } + + public IAsyncEnumerable GetAll() + { + return _db.Games.Select(i => new Game + { + Code = i.Id.ToBase64(), + IsActive = i.IsActive, + PlayerOneCode = i.PlayerOneId.ToBase64(), + PlayerTwoCode = i.PlayerTwoId.ToBase64(), + ScoreOne = i.ScoreOne, + ScoreTwo = i.ScoreTwo + }).AsAsyncEnumerable(); + } + } +} diff --git a/backend/Repositories/PingPongContext.cs b/backend/Repositories/PingPongContext.cs index c8c1489..adb31a8 100644 --- a/backend/Repositories/PingPongContext.cs +++ b/backend/Repositories/PingPongContext.cs @@ -7,6 +7,7 @@ public class PingPongContext : DbContext public DbSet Players { get; set; } public DbSet Sessions { get; set; } public DbSet SessionPlayers { get; set; } + public DbSet Games { get; set; } public PingPongContext(DbContextOptions options) : base(options) { } @@ -22,6 +23,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .ToTable("session"); + modelBuilder.Entity() + .ToTable("game"); + modelBuilder.Entity().HasOne(x => x.Session).WithMany(x => x.SessionPlayers).HasForeignKey(x => x.SessionId); } } diff --git a/backend/Startup.cs b/backend/Startup.cs index a07af15..f9c4986 100644 --- a/backend/Startup.cs +++ b/backend/Startup.cs @@ -34,6 +34,7 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddControllers(); } diff --git a/database/game.sql b/database/game.sql new file mode 100644 index 0000000..529e90d --- /dev/null +++ b/database/game.sql @@ -0,0 +1,27 @@ +CREATE TABLE [dbo].[game]( + [id] [uniqueidentifier] NOT NULL, + [player_one_id] [uniqueidentifier] NOT NULL, + [player_two_id] [uniqueidentifier] NOT NULL, + [score_one] [int] NULL, + [score_two] [int] NULL, + [is_active] [bit] NULL, + CONSTRAINT [PK_game] PRIMARY KEY CLUSTERED +( + [id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO + +ALTER TABLE [dbo].[game] WITH CHECK ADD CONSTRAINT [FK_game_player] FOREIGN KEY([player_one_id]) +REFERENCES [dbo].[player] ([id]) +GO + +ALTER TABLE [dbo].[game] CHECK CONSTRAINT [FK_game_player] +GO + +ALTER TABLE [dbo].[game] WITH CHECK ADD CONSTRAINT [FK_game_player1] FOREIGN KEY([player_two_id]) +REFERENCES [dbo].[player] ([id]) +GO + +ALTER TABLE [dbo].[game] CHECK CONSTRAINT [FK_game_player1] +GO diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index ce46313..0bcf044 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,10 +1,10 @@ import React from 'react'; import {render} from 'react-dom'; import {HashRouter as Router, Route, Switch} from "react-router-dom"; -import Games from "./views/games/games"; import PlayersList from "./views/players/players"; import {Shell} from "./views/shell/shell"; import Sessions from "./views/sessions/sessions"; +import GamesPage from "./views/games/games"; const App = () => ( @@ -12,7 +12,7 @@ const App = () => ( - + diff --git a/frontend/src/views/games/create_game.tsx b/frontend/src/views/games/create_game.tsx new file mode 100644 index 0000000..c07e66c --- /dev/null +++ b/frontend/src/views/games/create_game.tsx @@ -0,0 +1,40 @@ +import React, {useCallback, useState} from "react"; +import {withRouter} from "react-router"; +import {BackLink, DefaultButton} from "../../ui/back_button"; +import {Page} from "../shell/shell"; +import {BusyOverlay} from "../../ui/busy/busy"; +import {gamesApi} from "./games_api"; + +export const CreateGame = withRouter(({history}) => { + const [form, setForm] = useState<{ name: string, email: string }>({name: null, email: null}); + const [busy, setBusy] = useState(false); + + const onSubmit = useCallback(async (ev) => { + ev.preventDefault(); + setBusy(true); + await gamesApi.create({Email: form.email, Description: form.name}); + setBusy(false); + history.replace('/players'); + }, [form]); + + return ( + + + +
+ {busy && } + + + setForm({...form, name: e.target.value})}/> + + + setForm({...form, email: e.target.value})}/> + +
+ ) +}); diff --git a/frontend/src/views/games/games.tsx b/frontend/src/views/games/games.tsx index 1cf8d12..8fa0f1d 100644 --- a/frontend/src/views/games/games.tsx +++ b/frontend/src/views/games/games.tsx @@ -1,29 +1,60 @@ -import React from "react"; +import React, {useCallback} from "react"; import {cls} from "../../util/react"; import {Page} from "../shell/shell"; -import {gamesApi} from "./games_api"; +import {Game, gamesApi} from "./games_api"; import {List} from "../../ui/list/list"; +import {Link, Route, Switch, useRouteMatch, withRouter} from "react-router-dom"; +import {CreateGame} from "./create_game"; const css = require('./games.scss'); -const Games = () => { +const GamesPage = () => { + const {path} = useRouteMatch(); + + return ( + + + + + {/**/} + {/* */} + {/**/} + + + + + ); +}; + +const GamesList = withRouter(({history}) => { + const {url} = useRouteMatch(); + const getAll = useCallback(() => gamesApi.getAll(), []); + return ( - }/> + + +
+ } + data={getAll} + onItemClick={(i) => history.push(url + '/' + i.code)}/> +
) -}; +}); + + -const GameItem = ({game}) => -
-
{game.player1}
-
{game.score1}
+const GameItem = (props: Game) => +
+
{props.playerOneCode}
+
{props.scoreOne}
-
{game.score2}
-
{game.player2}
+
{props.scoreTwo}
+
{props.playerTwoCode}
; -export default Games; +export default GamesPage; diff --git a/frontend/src/views/games/games_api.ts b/frontend/src/views/games/games_api.ts index cb76229..27b4925 100644 --- a/frontend/src/views/games/games_api.ts +++ b/frontend/src/views/games/games_api.ts @@ -2,11 +2,12 @@ import {API_URL} from "../../config"; export type Game = { - // code: string; - // description: string; - // email: string; - // wins: number; - // losses: number; + code: string, + playerOneCode: string, + playerTwoCode: string, + scoreOne: number, + scoreTwo: number, + isActive: boolean } export const gamesApi = {