This project is a replica of the Netmatters (NM) website, coded in HTML, Sass, JS, jQuery and PHP.
Find the project posted Here
Find the GitHub link Here
The main goal of this project was to reflect on each language and apply the knowledge as I learnt it. Some code examples are below:
For the NM facsimile, PHP is utilised to retrieve and display information from the DB in the form of news cards and also to submit user form data to the database. The first two includes here are used to connect to the db (db_handler.php) and another that uses that connection to input form data into the DB (form_submit.php).
db_handler.php
// the servername, username and password here are set to local default
$dbHostName = "localhost";
$dbUserName = "root";
$dbPassword = "";
$dbName = "nm_db";
// the variables above are created in the order in which they need to be passed
// on to the mysqli_connect() function.
$conn = mysqli_connect($dbHostName, $dbUserName, $dbPassword, $dbName);
db_handler.php is then utilised as an include in form_submit.php:
form_submit.php
<?php include_once($_SERVER['DOCUMENT_ROOT'] . "/inc/dbh.inc.php");
/*
This file deals with entering the contact information into the database. It
also uses the variables created for the db entry to send an email with some of
the contact details, subject line and a message. The database was initialised
via an sql query found in the sql folder.
*/
// Defines all the vairables for db input. Escapes any malicious characters to
// avoid code injection.
$first = mysqli_real_escape_string($conn, $_POST['first']);
$last = mysqli_real_escape_string($conn, $_POST['last']);
$email = mysqli_real_escape_string($conn, $_POST['email']);
$date = date('Y-m-d H:i:s');
$subject = mysqli_real_escape_string($conn, $_POST['subject']);
$message = mysqli_real_escape_string($conn, $_POST['message']);
$gdprChecked = "";
// Checks if the checkbox is checked or unchecked and stores info in variable.
// (doesn't need escaping as it returns a bool).
if(isset($_POST['gdpr'])) {
$gdprChecked = "true";
} else {
$gdprChecked = "false";
}
// Prepares sql query with variable info ready to be input in the db.
$sql = "INSERT INTO contact_details (first_name, last_name, email,
date, subject, message, gdpr) VALUES ('$first', '$last', '$email',
'$date', '$subject', '$message', '$gdprChecked');";
/*
Checks if requisite fields are filled in and that the email is legitimate.
if so it connects to the database and queries in the info and changes the
header, if not it changes the header to submit fail.
*/
if ($first && $last && $email && filter_var($email, FILTER_VALIDATE_EMAIL)) {
mysqli_query($conn, $sql);
header("Location: ../pages/contact.php?submit=success");
} else {
header("Location: ../pages/contact.php?submit=fail");
}
// If you wish to change which address the email is sent to, change '$to'
$to = "alex.mckendrick@gmail.com";
$msg = "$message\n\n$first $last\n\nGDPR box $gdprChecked\n\n$email\n\n$date";
/*
(Windows only) When PHP is talking to a SMTP server directly, if a full stop
is found on the start of a line, it is removed. To counter-act this,
str_replace is used to replace these occurrences with a double dot.
*/
$msg = str_replace("\n.", "\n..", $msg);
// Word wrap for neat formatting of messages longer than 70 chars
$msg = wordwrap($msg, 70);
mail($to, $subject, $msg);
The following include queries the database for the 3 latest news cards in preparation to display the data in a for each loop on the page.
news_fetch.php
//include contains database handler which uses msqli to connect to db.
<?php include_once($_SERVER['DOCUMENT_ROOT'] . "/inc/dbh.inc.php");
// write query for the 3 cards I want to display on the
// page by DESC to get the latest in the table
$news = "SELECT * FROM news_cards ORDER BY date DESC LIMIT 3";
// retrieve the result set (set of rows).
$retNews = mysqli_query($conn, $news);
// fetch the resulting rows as an array.
$cards = mysqli_fetch_all($retNews, MYSQLI_ASSOC);
// free $retNews from memory (good practice).
mysqli_free_result($retNews);
// close connection.
mysqli_close($conn);
The following include queries the database for the 3 latest news cards in preparation to display the data in a for each loop on the page.
card_inc.php
<?php foreach($cards as $card){ ?>
<div class="news-containers">
<div class="news-blocks">
<div class="background1">
<img src="<?php echo $card['image_url']; ?>"
alt="<?php echo $card['image_name']; ?>"
class="news-image grow">
<a href="#">
<div class="news-tag"
style="background-color:<?php echo $card['card_color']; ?>"
><?php echo $card['card_category']; ?></div>
</a>
</div>
<div class="info-date">
<div class="news-info">
<div>
<a href="#" id="news-link-style-1">
<h3 style="color:<?php echo $card['card_color']; ?>"
class="news-title"
>
<?php echo $card['card_title']; ?>
</h3>
</a>
</div>
<div class="news-p">
<?php echo substr($card['card_content'], 0, 85) . "..."; ?>
</div>
<a href="#" class="news-btn-con">
<div style=
"
background-color:<?php echo $card['card_color']; ?>
"
class="read-more read1 btn-drkn">
Read more
</div>
</a>
</div>
<div class="footer-wrapper">
<div class="news-footer">
<div class="mini-logo">
<img src=
"
<?php echo $card['icon_url']; ?>
"
alt=
"
<?php echo $card['icon_name']; ?>
"
style="border-radius:100%;">
</div>
<div class="news-foot-txt">
<div class="posted">
<h4>
Posted by <?php echo $card['card_author']; ?>
</h4>
</div>
<div class="break"></div>
<div class="date">
<span><?php echo $card['date']; ?></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<?php } ?>
Here I wrote an statement to create the table for the news cards for the Netmatters site. I wanted to be able to add inline styling so I had control over the colours and have the appropriate placeholder attributes for the image titles for accessibility.
CREATE TABLE news_cards (
id int(11) not null PRIMARY KEY AUTO_INCREMENT,
card_color varchar(30) not null,
color_drk varchar(30) not null,
card_category varchar(25) not null,
image_name varchar(150) not null,
image_url varchar(6000) not null,
card_title varchar(100) not null,
card_content varchar(150) not null,
icon_name varchar(150) not null,
icon_url varchar(6000) not null,
card_author varchar(35) not null,
date varchar(30) not null
);
and to further practice SQL, I wrote insert statements for each of the cards, here is one as an example:
INSERT INTO
news_cards (card_color, color_drk, card_category,
image_name, image_url, card_title, card_content,
icon_name, icon_url, card_author, date)
VALUES (
'#926fb1',
'#79539a',
'careers',
'now hiring',
'/img/news/news-hiring.jpg',
'Office Administrator/Receptionist',
'Salary:£18k-£24k + Bonuses +
Pension Hours :40 hours per week,
Monday - Friday Location: W...',
'nm logo',
'/img/mini-logo.png',
'Netmatters Ltd',
'8th March 2021'
);
JS was implemented for two animations on the NM site;
The sticky nav and the pop-in sidebar
This sticky nav JS was written as a polyfill, as cross browser compatibility with IE
was one of the aims of this project. It was written in vanilla JS to practice the syntax.
(function(){
// variable declarations for abbreviating DOM selectors.
const doc = document.documentElement;
const w = window;
// Define four variables: curScroll, prevScroll, curDirection, prevDirection
let curScroll;
let prevScroll = w.scrollY || doc.scrollTop;
let curDirection = 0;
let prevDirection = 0;
/*
how it works:
-------------
create a scroll event listener
create function to check scroll position on each scroll event,
compare curScroll and prevScroll values to find the scroll direction
scroll up - 1, scroll down - 2, initial - 0
then set the direction value to curDirection
compare curDirection and prevDirection
if it is different, call a function to show or hide the header
example:
step 1: user scrolls down: curDirection 2, prevDirection 0 > hide header
step 2: user scrolls down again:
curDirection 2, prevDirection 2 > already hidden, do nothing
step 3: user scrolls up: curDirection 1, prevDirection 2 > show header
*/
let header = document.getElementById('nav');
let toggled;
let threshold = 300;
let checkScroll = function() {
curScroll = w.scrollY || doc.scrollTop;
if(curScroll > prevScroll) {
// scrolled down
curDirection = 2;
}
else {
//scrolled up
curDirection = 1;
}
if(curDirection !== prevDirection) {
toggled = toggleHeader();
}
prevScroll = curScroll;
if(toggled) {
prevDirection = curDirection;
}
};
let toggleHeader = function() {
toggled = true;
if(curDirection === 2 && curScroll > threshold) {
header.classList.add('hide');
}
else if (curDirection === 1) {
header.classList.remove('hide');
}
else {
toggled = false;
}
return toggled;
};
window.addEventListener('scroll', checkScroll);
})();
Another issue to tackle with cross browser compatibility is differing scroll-bar widths, so a class is written here to produce a width variable for each browsers scroll-bar:
sidebar.js
// Temp scroll box was written to measure the width
// of the scroll-bar for cross-browser compatibility
class TempScrollBox {
constructor() {
this.scrollBarWidth = 0;
this.measureScrollbarWidth();
}
measureScrollbarWidth() {
// Add temporary box to wrapper
let scrollbox = document.createElement('div');
// Make box scrollable
scrollbox.style.overflow = 'scroll';
// Append box to document
document.body.appendChild(scrollbox);
// Measure inner width of box
this.scrollBarWidth = scrollbox.offsetWidth - scrollbox.clientWidth;
// Remove box
document.body.removeChild(scrollbox);
}
get width() {
return this.scrollBarWidth;
}
}
// Stores the variable returned from the class so that scroll bar width can be
// calculated.
let scrollbox = new TempScrollBox();
window.onload = function() {
var width = $(window).width();
// transform and transition times are added as 0s and linear so they don't
// trigger an animation when the sidebar is added.
$(".sb").css("transition", `0s`);
$(".sb").css("transition-timing-function", `linear`);
if (width >= 975) {
// Allows the sidebar size to be determined, using the scroll bar calculator
var transformValue = `${scrollbox.width + 333}`;
$(".sb").css("transform", `translateX(${transformValue}px)`);
$(".sb").css("width", `${transformValue}px`);
console.log(transformValue);
}
else if (width <975) {
var transformValue = `${scrollbox.width + 257}`;
$(".sb").css("transform", `translateX(${transformValue}px)`);
$(".sb").css("width", `${transformValue}px`);
console.log(transformValue);
}
};
$(window).resize(function() {
//window width has to be locally scoped as it changing dynamically with resize
var width = $(window).width();
const shiftBody = $("#nav, main, footer");
if (width >= 992) {
/* This detects whether the burger menu is active or not
(hamburger-mask being present = menu active). if the sidebar properties are
changed while the menu is active, the sidebar disappears, so only the
variable is defined when it is active
*/
if (document.getElementById('mask').classList.contains("hamburger-mask")) {
var transformValue = `${scrollbox.width + 333}`;
let transformStart = `translateX(-${transformValue}px)`;
$(".sb").css("transform", `translateX(0)`);
$(".sb").css("width", `${transformValue}px`);
shiftBody.css("transition", `0s`);
shiftBody.css("transition-timing-function", `linear`);
shiftBody.css("transform", `${transformStart}`);
} else {
var transformValue = `${scrollbox.width + 333}`;
$(".sb").css("transform", `translateX(${transformValue})`);
$(".sb").css("transition", `0s`);
$(".sb").css("transition-timing-function", `linear`);
$(".sb").css("width", `${transformValue}px`);
}
}
else if (width <992) {
if (document.getElementById('mask').classList.contains("hamburger-mask")) {
var transformValue = `${scrollbox.width + 257}`;
let transformStart = `translateX(-${transformValue}px)`;
$(".sb").css("transform", `translateX(0)`);
$(".sb").css("width", `${transformValue}px`);
shiftBody.css("transition", `0s`);
shiftBody.css("transition-timing-function", `linear`);
shiftBody.css("transform", `${transformStart}`);
}
else {
var transformValue = `${scrollbox.width + 257}`;
$(".sb").css("transform", `translateX(${transformValue}px)`);
$(".sb").css("transition", `0s`);
$(".sb").css("transition-timing-function", `linear`);
$(".sb").css("width", `${transformValue}px`);
}
}
}
);
//Hamburger menu button is clicked
$(".hamburger").click(function () {
//Scrollbar is removed from body.
$("#sb-width").removeClass("no-scroll");
$("body").addClass("no-scroll");
// Variables are declared locally to call on the current sidebar width
// defined by onload or the window size listener.
var transformValue = $(".sb").width();
let transformStart = `translateX(-${transformValue}px)`;
let transformIn = `translateX(0)`;
$(".sb").css("transition", "transform 0.4s");
$(".sb").css("transition-timing-function", "ease-in");
//this class is from the hamburger animation script to trigger the animation.
$(".hamburger").addClass("is-active");
// the 'nav' main and footer is shifted left.
shiftBody.css("transform", `${transformStart}`);
// the class that darkens the 'body' is added.
$(".mask-click").addClass("hamburger-mask");
// the sidebar is transformed Into view
$(".sb").css("transform", `${transformIn}`);
// Fires carousel the next slide so a visual glitch doesn't occur on it.
$('.carousel').slick('slickNext');
});
$(".mask-click").click(function () {
// the scrollbar is added back to the body
$("#sb-width").addClass("no-scroll");
$("body").removeClass("no-scroll");
// Variables are declared locally to call on the current sidebar width
// defined by onload or the window size listener.
var transformValue = $(".sb").width();
let transformStart = `translateX(-${transformValue}px)`;
let transformIn = `translateX(0)`;
$(".sb").css("transition", "transform 0.4s");
$(".sb").css("transition-timing-function", "ease-in");
shiftBody.css("transition", "transform 0.4s");
shiftBody.css("transition-timing-function", "ease-in");
//the animation state is removed from the hamburger icon
$(".hamburger").removeClass("is-active");
// the 'body' is moved back to its original position
shiftBody.css("transform", `${transformIn}`);
// the sidebar (sb) is moved off screen.
$(".sb").css("transform", `translateX(${transformValue}px)`);
// the dark overlay (aka mask) is removed from the 'body'.
$(".mask-click").removeClass("hamburger-mask");
});
// This is to highlight the correct items on hover for the sidebar
$('.sb--ul').hover(
function() {
$(this).siblings().children().css(
'background-color', 'rgba(102,102,102, 0.15)');
},function(){
$(this).siblings().children().css('background-color', '#2333645');
}
);
// A second sidebar exists at a breakpoint with different content
// which is what sb--ul-2 is selecting
$('.sb--ul-2').hover(
function() {
$(this).siblings().children().css(
'background-color', 'rgba(102,102,102, 0.15)');
}, function(){
$(this).siblings().children().css('background-color', '#2333645');}
);
Due to this starting off as the first website implementing Sass, there are a lot of mistakes I made along the way. The most important lessons I learned are:
Halfway through styling the NM site, I came across the BEM naming methodology and 7-1 partial architecture,
which ever since I have used as a loose guideline for naming and structuring my selectors and style sheets.
It has made life so much easier for me; nesting BEM selectors makes style sheets so much more readable and
maintainable. It also makes Breakpoints incredibly easy to manage.
Modularity and maintainability is what I look for in any language or framework now, as the technical debt it
can save me is invaluable.
Below is an example of the CSS used to style the hover tooltips for the client logos.
.arrow-box,
.arrow {
--scale: 0;
--arrow-size: 30px;
position: absolute;
left: 50%;
transform: translatex(-50%)
translateY(var(--translate-y, 0))
scale(var(--scale));
top: -.25rem;
}
.client-img:hover {
.arrow-box {
--scale: 1;
}
.arrow {
--scale: 1;
}
}
.arrow-box {
--translate-y: calc(-100% - var(--arrow-size));
width: 260px;
height: auto;
padding: 20px;
font-family: poppins;
text-align: center;
color: white;
background: $dark-bg;
}
.arrow {
--translate-y: calc(-1 * var(--arrow-size));
border: var(--arrow-size) solid transparent;
border-top-color: $dark-bg;
}