Description
Developed for a web application development module at university, CineWAD is a fake cinema chain website with five main areas of focus:
- HTML/CSS Front End - Design a suitably attractive front end using HTML and CSS. The site should have a consistent ālook and feelā. Marks were available for designs that attempted to make the site mobile friendly, consider accessibility, and implement good SEO practises.
- Database Design and Modelling design - Structure and populate a Relational Database to hold the event information using Entity Framework models. Appropriate data types should be used and must include dates and/or times. Marks were available for the application of authorisation and authentication techniques.
- Javascript - Use JavaScript to enhance the User Interactions and User Experience. This may include animations, slide shows, form validation and data transactions via AJAX. Marks were allocated based on the originality and complexity of the Javascript features added. I was free to use Javascript libraries and frameworks as long as they were credited.
- C# Code - Use the .NET MVC design pattern to produce an application that demonstrates separation concerns. Use of layout views and their various features to ease page maintenance. Integrate the backend database using C# to provide users with event information. Use server side scripting to add features such as data filtering and pagination.
- Content Management System - Build a bespoke CMS to allow the site owner to add, amend and delete event information. This should be password protected. Marks were allocated for work that illustrated an understanding of web application security.
Features
Homepage Carousel
I didnāt dedicate too much time to the styling of the website due to time constraints, though I did implement a Flickity carousel on the homepage. This carousel is populated by client-side calls to an endpoint I built that returns JSON encoded data such as the most popular, recent, or featured films.
This action returns future releases:
[HttpGet]
[Produces("application/json")]
[Route("{controller}/{action}")]
[Route("{controller}/{action}/{limit:int}")]
public IActionResult ComingSoon(int limit = 3)
{
if (limit <= 0)
{
return new JsonResult(new List<Film>());
}
List<Film> model = _context.Films.Where(film => film.ReleaseDate > DateTime.Now).OrderBy(film => film.ReleaseDate).Take(limit).ToList();
return new JsonResult(model, new JsonSerializerOptions
{
WriteIndented = true,
});
}
I used an autonomous custom element to reliably render film thumbnails with their title, image, and year. It explicitly avoids the shadow DOM so that I can style thumbnails alongside the rest of the website.
class FilmThumbnail extends HTMLElement {
constructor() {
super();
}
set film(film) {
let childDiv = document.createElement("div");
let anchor = document.createElement("a");
let img = document.createElement("img");
img.src = "";
anchor.href = "Home/AllFilms";
let titleStr = "";
if (film) {
let yr = new Date(film.releaseDate || film.ReleaseDate).getFullYear(); //these OR conditions handle variations in how the server serializes keys
titleStr = (film.filmTitle || film.FilmTitle) + " (" + yr + ")";
anchor.href = "Home/FilmDetails/" + (film.filmID || film.FilmID);
anchor.title = titleStr;
anchor.style.textDecoration = "none";
img.alt = titleStr;
img.src = film.filmImage || film.FilmImage;
img.style.height = "100%";
img.style.width = "100%";
img.style.objectFit = "contain";
}
anchor.appendChild(img);
childDiv.appendChild(anchor);
this.innerHTML = childDiv.outerHTML;
this.className = "filmThumb";
this.style.display = "block";
}
}
//register the class with the DOM
window.customElements.define("film-thumbnail", FilmThumbnail);
Mapping and Preferring Locations
A cinema would be useless if you donāt know how to find it! The locations page implements a MapBox element with custom CineWAD markers. The data for the markers is inserted into the Mapbox script via ViewData. So that the page is easy to use, the view action can accept a search term and/or lat,long coordinates.
Results are filtered to only those that contain the search term, if one has been provided. After that, the filtered results are sorted according to their distance from the user, either through their coordinates if they allow browser location permissions or by using the Mapbox API to geocode their search term.
If a user is logged in to the website they can choose which location they prefer. This will be shown first in results and is used to prefill fields when searching for a showing. As staff users work at a fixed location, they cannot prefer a location and must ask their boss, or another administrator, to change their preferred location for them.
Showings
The primary focus of the website is to get users to book tickets for showings. For this reason I created a reusable partial component that submitted a GET request via a form to the āShowingsā action. This form was populated from my database tables and simply redirected the user to the showings view.
I normalised the database tables which meant that in order to display information related to the Showing model I used a ViewModel to combine objects together. It allowed me to easily show information relating to each showingās film, screen, and location together.
public class Showing
{
[Key][Required]
public int ShowingID { get; set; }
[Required]
[ForeignKey("Location")]
public int LocationID { get; set; }
[Required]
[ForeignKey("Film")]
public int FilmID { get; set; }
[Required]
public int ScreenNumber { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime StartDatetime { get; set; }
[Required]
[Range(2, 4)]
public int Dimensions { get; set; }
}
//complete joins on the tables to return all revelant info from across the database tables as a ViewModel
var query = from showing in _context.Showings
join film in _context.Films on showing.FilmID equals film.FilmID
join location in _context.Locations on showing.LocationID equals location.LocationID
join screen in _context.Screens on new { loc = location.LocationID, scr = showing.ScreenNumber } equals new { loc = screen.LocationID, scr = screen.ScreenNumber }
where showing.StartDatetime >= DateTime.Now
select new ShowingHelper(showing, film, location, screen, _context.Bookings.Where(b => b.ShowingID == showing.ShowingID).ToList());
As their could be a large number of showings at any one time, a LINQ query is used to paginate results on the server before they are sent to the client.
public IActionResult Showings(int? page, int? pageSize, ...)
//set up pagination
int pSize = Math.Max(1, pageSize ?? 10);
int pageNumber = Math.Max(0, page ?? 0);
ViewData["pageSize"] = pSize;
ViewData["pageNumber"] = pageNumber;
//join, filter, and sort code has been omitted
//then paginate
var model = queryList.Skip(pageNumber * pSize).Take(pSize).ToList();
return View(model);
Database and Modelling Design
I normalized the data in my database in order to reduce redundancy and keep it maintainable. This meant that I had to make sure I had primary and foreign key metadata attributes on my models during set up so that CRUD operations wouldnāt ruin the dataās integrity by altering or removing records.
Using attributes in the Showing class to set up the database:
[Required]
[ForeignKey("Location")]
public int LocationID { get; set; }
[DataType(DataType.Date)]
public DateTime StartDatetime { get; set; }
Adding extra attributes to identity users was as simple as extending the class. āBirthDateā was used when checking a persons age against a filmās certification and āPreferredLocationā was used to customize the customer experience.
public class AppIdentityUser : IdentityUser
{
public DateTime BirthDate { get; set; }
public int PreferredLocation { get; set; }
}
//accessing the extended class in the controller
AppIdentityUser fullUser = _userManager.GetUserAsync(User).Result;
CMS
The content manangement system for CineWAD uses .NET Core Identity Services to facilitate three roles:
- Customer:
- can book tickets for themselves and edit their own bookings.
- Staff:
- can add & edit bookings for any user.
- can edit the details of Customer users.
- can add & edit showings.
- Admin:
- Staff permissions plus
- can edit Staff and other Admins
- can edit user roles
- can add & remove films
- can edit homepage slides
Authorization attributes were set inside the CMS and API controllers in order to limit access:
[Authorize(Roles = "Staff,Admin")]
public IActionResult EmployeeIndex(){}
Then within View pages, conditional content is rendered based on a users role. This can be seen in the CMS screenshots. The example below shows one condition displayed when adding a booking. Customers can see their own email, but employees can change who the booking is for. In case of malicious actors, the endpoint is validated server-side once the form is submitted, so bookings canāt be added without permission.
//if the user's role is Staff or Admin, allow them to change who this booking is for
//otherwise default on a fixed input
<div>
@if ((bool)ViewData["IsEmployee"])
{
<select asp-for="UserEmail">
@foreach (IdentityProject.Security.AppIdentityUser user in ((List
<IdentityProject.Security.AppIdentityUser>
)ViewData["Customers"]))
{
<option value="@user.Email">@user.FullName (@user.Email)</option>
}
</select>
}
else
{
<input type="hidden" asp-for="UserEmail" />
<p>Email: @Model.UserEmail</p>
}
</div>
QR Codes
When going to a cinema, itās common now to show QR codes instead of paper tickets. As I had the CMS set up, it was fairly simple for me to throw in the qrcodejs and html5-qrcode libraries. Qrcodejs was used on the customer side so that they could show a code to staff and html5-qrcode was used on the staff/admin side to verify customer tickets and direct them to their seats. The image here shows all possible warnings that could be displayed to a staff member.
Final Notes
Thereās work to be done in order to make the site feel polished and look mobile friendly. I didnāt add the option to edit a film to the CMS, only add or delete. This is something that could be implemented with a small amount of effort. Overall though, Iām happy with this project and the learning I achieved through it.