CineWAD#

A university project exploring .NET web development

-

c-sharp icon dot-net icon javascript icon 🏫 🌐

CineWAD

Description

Developed for a web application development module at university, CineWAD is a fake cinema chain website with five main areas of focus:

Features

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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

Locations page
Locations page
Geocoded results
Geocoded result

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.

Location action flowchart
Location action flowchart

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

Showings component
Showings component

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.

ShowingHelper ViewModel
ShowingHelper ViewModel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    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.

1
2
3
4
5
6
7
8
9
10
11
12
13
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

Films database table
Films table
Showings database table
Showings table

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:

1
2
3
4
5
6
7
[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.

1
2
3
4
5
6
7
8
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:

Authorization attributes were set inside the CMS and API controllers in order to limit access:

1
2
[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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//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>
Add Showing
Add Showing
Showing list
Showing list
Admin View
Admin View
Customer View
Customer View
Edit booking
Edit booking
Edit person
Edit person

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.

Customer QR Page
Customer Booking QR Code
Staff QR Page
Staff Ticket Validation

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.