From 0acfbd61d1f1ccaecf4828f864113dd9bf12f0a0 Mon Sep 17 00:00:00 2001
From: Immanuel Onyeka <immanuel@onyeka.ca>
Date: Sun, 18 Jul 2021 14:21:46 -0400
Subject: [PATCH] Initial commit

---
 .gitignore            |   1 +
 BillingController.php | 278 ++++++++++++++++
 README.md             |   6 +
 main.scss             | 745 ++++++++++++++++++++++++++++++++++++++++++
 panel.vue             | 129 ++++++++
 5 files changed, 1159 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 BillingController.php
 create mode 100644 README.md
 create mode 100644 main.scss
 create mode 100644 panel.vue

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..45d62d8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.sw?
diff --git a/BillingController.php b/BillingController.php
new file mode 100644
index 0000000..7042ea2
--- /dev/null
+++ b/BillingController.php
@@ -0,0 +1,278 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Stripe\Stripe;
+use Stripe\Customer;
+use Stripe\PaymentIntent;
+use Stripe\PaymentMethod;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Auth;
+use App\Models\Transaction;
+
+class BillingController extends Controller
+{
+	protected $stripe;
+	protected $user;
+
+
+	public function __construct() {
+		$this->stripe = new \Stripe\StripeClient(config('services.stripe.secret'));
+		Stripe::setApiKey(config('services.stripe.secret'));
+		$this->user = Auth::user();
+	}
+
+	protected function attempt($packs) {
+		$user = Auth::user();
+
+		foreach($packs as $value) {
+			if ($value < 0) {
+				abort(422);
+			}
+		}
+
+		$amount = $packs[ 'credits10' ]*1099 +
+			$packs[ 'credits50' ]*5499 + $packs[ 'credits100' ]*10999
+			+ $packs[ 'credits1000' ]*101000;
+
+		$transaction = new Transaction;
+		$transaction->credits = $packs['credits10']*1000 +
+			$packs['credits50']*5000 +
+			$packs['credits100']*10000 +
+			$packs['credits1000']*100000;
+		$transaction->credits_extra =
+			$packs['credits50']*500 +
+			$packs['credits100']*1000 +
+			$packs['credits1000']*15000;
+
+		$transaction->user_id = $user->id;
+		$transaction->charge = $amount;
+		$transaction->status = 'processing';
+		$transaction->completed = false;
+		$transaction->save();
+		return $transaction;
+	}
+
+	//Expects an array 'packs' representing the amount of each multiple of credits.
+	//Should validate that all amounts are positive integers in a reasonable range
+	public function stripeSecret(Request $request) {
+		$user = Auth::user();
+		$transaction = $this->attempt($request->packs);
+
+		$intent = PaymentIntent::create([
+			'amount' => $amount,
+			'currency' => 'usd',
+			'customer' => $user->customer_id,
+			'description' => "You have received $total_credits credits.",
+			'receipt_email' => Auth::user()->email,
+			'metadata' => ['transaction_id' => $transaction->id]
+		]);
+		$transaction->intent_id = $intent->id;
+
+		//Save the card as a default if none is set and it was selected
+		if ($user->payment_method == null && $request->card) {
+			$this->changeDefaultCard($request->card);
+		}
+
+		$transaction->save();
+		return $intent->client_secret;
+	}
+
+	public function getCards() {
+		return PaymentMethod::all([
+			'customer' => Auth::user()->customer_id,
+			'type' => 'card'
+		]);
+	}
+
+	//Adds correct credit amount to the charged user, precise to two decimal places 
+	public function chargeEvent(Request $request) {
+		$event = \Stripe\Event::constructFrom($request->all());
+		$charge = $event->data->object;
+		$transaction = Transaction::where('intent_id', $charge->payment_intent)->first();
+
+		if ($event->type == 'charge.succeeded') {
+			$this->creditUser($transaction->id);
+		} else {
+			$transaction->status = $charge->status;
+			$transaction->save();
+		}
+	}
+
+	public function changeDefaultCard(String $card) {
+		$user = Auth::user();
+		$user->payment_method = $card;
+		$user->save();
+		$cards = $this->getCards();
+
+		return PaymentMethod::all([
+			'customer' => Auth::user()->customer_id,
+			'type' => 'card'
+		]);
+	}
+
+	public function deleteCard(Request $request) {
+		$this->stripe->paymentMethods->detach($request->card);
+
+		$user = Auth::user();
+		if ($request->card == $user->payment_method) {
+			$user->payment_method = null;
+			$user->save();
+		}
+		return ($this->getCards());
+	}
+
+	//Receives a request with a packs. It is an array of each type of credit
+	//amount to be bought
+	public function payeer(Request $request) {
+		$user = Auth::user();
+		$transaction = $this->attempt($request->packs);
+		$shopid = config('services.payeer.id');
+		$secret = config('services.payeer.secret');
+		$param_key = config('services.payeer.param_key');
+		$total = $transaction->credits/100 + $transaction->credits_extra/100;
+		$description =  base64_encode("You will receive $total credits.");
+
+		$arHash = [$shopid, $transaction->id, $transaction->charge/100, 'USD',
+			$description];
+
+		$params = ['reference' => ['transaction_id' => $transaction->id]];
+		$key = md5($param_key.$transaction->id);
+		$encodedParams = @urlencode(base64_encode(openssl_encrypt(
+			json_encode($params), 'AES-256-CBC', $key, OPENSSL_RAW_DATA
+		)));
+		$arHash[] = $encodedParams;
+		$arHash[] = $secret;
+
+		$signature = strtoupper(hash('sha256', implode(':', $arHash)));
+		$user->paying = true; $user->save();
+
+		return [ 'signature' => $signature, 'params' => $encodedParams, 'shop'
+			=> $shopid, 'transaction' => $transaction->id, 'amount' =>
+			$transaction->charge/100, 'description' => $description ];
+	}
+
+	//This needs to check the ip of the sender
+	public function processPayeer(Request $request) {
+		$allowed = ['185.71.65.92', '185.71.65.189', '149.202.17.210'];
+		$ipAddress = $request->ip();
+
+		if (!in_array($ipAddress, $allowed)){
+			abort(401);
+		}
+
+		Log::debug('Processing Payeer payment');
+		Log::debug($request);
+
+		$secret = config('services.payeer.secret');
+		$arHash = [$request->m_operation_id,
+			$request->m_operation_ps,
+			$request->m_operation_day,
+			$request->m_operation_pay_date,
+			$request->m_shop,
+			$request->m_orderid,
+			$request->m_amount,
+			$request->m_curr,
+			$request->m_desc,
+			$request->m_status
+		];
+
+		if (isset($request->m_params)) {
+			$arHash[] = $request->m_params;
+		}
+
+		$arHash[] = $secret;
+
+		$signature = strtoupper(hash('sha256', implode(':', $arHash)));
+		if ($signature == $request->m_sign && $request->m_status == 'success'){
+			$this->creditUser((int) $request->m_orderid);
+			return $request->m_orderid.'|success';
+		} else {
+			$transaction = Transaction::find($request->orderid);
+			$transaction->status = 'error';
+			$transaction->save();
+			return $request->m_orderid.'|error';
+		}
+	}
+
+	//Credits the user of a given transaction id
+	public function creditUser($transaction_id) {
+		$transaction = Transaction::find($transaction_id);
+
+		if ($transaction->completed) {
+			abort(422, 'Bad transaction ID');
+		}
+
+		$user = $transaction->user;
+		$user->credits = $user->credits + $transaction->credits +
+			$transaction->credits_extra;
+		$transaction->status = 'completed';
+		$transaction->completed = true;
+
+		$user->save();
+		$transaction->save();
+	}
+
+	public function pm(Request $request) {
+		$user = Auth::user();
+		$account = config('services.pm.account');
+		$transaction = $this->attempt($request->packs);
+		$total = $transaction->credits/100 +
+			$transaction->credits_extra/100;
+		$description =  "You will receive $total credits.";
+
+		$user->paying = true; $user->save();
+
+		return ['account' => $account, 'transaction' => $transaction->id,
+			'amount' => $transaction->charge/100, 'description' => $description];
+	}
+
+	//Handler run after PM payment succeds
+	public function processPM(Request $request) {
+		$allowed = ['77.109.141.170', '91.205.41.208', '94.242.216.60',
+			'78.41.203.75'];
+		$transaction = Transaction::find($request->PAYMENT_ID);
+		$secret = config('services.pm.secret');
+
+		//Check that sender is PM and account the amount was paid to is mine.
+		if (!in_array($request->ip(), $allowed)){
+			abort(401);
+		} else if ($request->PAYEE_ACCOUNT != config('services.pm.account')) {
+			abort(422);
+		} else if (!$transaction->complete) {
+			abort(422);
+		}
+
+		Log::debug('Processing PM payment');
+		Log::debug($request);
+
+		//Would need to be changed if baggage fields are used
+		$arHash = [$request->PAYMENT_ID,
+			$request->PAYEE_ACCOUNT,
+			$request->PAYMENT_AMOUNT,
+			$request->PAYMENT_UNITS,
+			$request->PAYMENT_BATCH_NUMBER,
+			$request->PAYER_ACCOUNT,
+			strtoupper(md5($secret)),
+			$request->TIMESTAMPGMT,
+		];
+
+		$signature = strtoupper(md5(implode(':', $arHash)));
+
+		if ($signature == $request->V2_HASH){
+			$this->creditUser((int) $transaction->id);
+		} else {
+			abort(422, 'Bad hash');
+		}
+	}
+
+	public function completePM(Request $request) {
+		return redirect('/panel/#transaction-complete');
+	}
+
+	public function failPM(Request $request) {
+		return redirect('/panel/#transaction-failed');
+	}
+
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c01f874
--- /dev/null
+++ b/README.md
@@ -0,0 +1,6 @@
+
+##Code Examples
+Some sample code taken from sites I've completed. They're written in
+Javascript, PHP, and SCSS. The examples are incomplete and meant for
+demonstrative purposes only as I'm not comfortable share complete code of a
+live, potentialy profitable project.
diff --git a/main.scss b/main.scss
new file mode 100644
index 0000000..c091886
--- /dev/null
+++ b/main.scss
@@ -0,0 +1,745 @@
+@use "vars";
+
+@font-face {
+	font-family: "PatuaOne";
+	src: url("../PatuaOne-Regular.ttf") format("opentype");
+	font-style: normal;
+	font-weight: normal;
+}
+
+html body {
+	font-family: 'PatuaOne', 'Times New Roman';
+	margin: 0;
+}
+
+section {
+	position: relative;
+	padding-top: 3em;
+	padding-bottom: 3em;
+	overflow: hidden;
+	padding-right: 15px;
+	padding-left: 15px;
+}
+
+button {
+	font-family: 'PatuaOne', 'Times New Roman';
+}
+
+textarea {
+	border: 1px solid grey;
+}
+
+h1 {
+	color: white;
+}
+
+h2 {
+	color: vars.getColor("brand-orange");
+	text-align: center;
+}
+
+h3 {
+	color: vars.getColor("faded-text");
+	// text-transform: uppercase;
+}
+
+ul {
+	color: vars.getColor("faded-text");
+	list-style-type: none;
+	padding: 0;
+}
+
+input, select {
+	border: 2px solid black;
+	border-radius: 20px;
+	padding: 4px;
+}
+
+.icon {
+	width: 25px;
+	margin: 10px;
+}
+
+.note-grey {
+	color: grey;
+	font-size: 0.8em;
+	max-width: 70%;
+}
+
+.services-cards li {
+	padding-bottom: 8px;
+	&:before {
+		width: 30px;
+		height: 30px;
+		content: url("../images/green-check.svg");
+		display: inline;
+		margin-right: 5px;
+		// top: 30px;
+	}
+}
+
+a {
+	text-decoration: none;
+}
+
+nav {
+	position: absolute;
+	width: 100%;
+	top: 10px;
+	display: flex;
+	flex-flow: row wrap;
+	align-items: center;
+	justify-content: space-between;
+	// background-color: transparent;
+}
+
+nav h1 {
+	margin: 4px 0 10px 6px;
+	display: inline-block;
+}
+
+nav.info-page h1 {
+	margin: 4px 0 10px 6px;
+	display: inline-block;
+	color: vars.getColor('brand-orange');
+}
+
+.formal-font {
+	font-family: "FreeSans";
+}
+
+nav #logo {
+	margin-right: 50px;
+}
+
+nav form {
+	display: flex;
+	flex-wrap: wrap;
+	margin-right: 20px;
+}
+
+nav form div {
+	margin: 0 10px;
+}
+
+nav form button {
+	min-width: 5em;
+}
+
+nav form p {
+	margin-top: 0;
+}
+
+nav form input,label {
+	opacity: 0.7;
+}
+
+nav .nav-toggle {
+	position: absolute;
+	right: 10px;
+	top: 5px;
+	display: none;
+	height: 45px;
+	width: 45px;
+	margin-left: auto;
+	background: url("../images/menu-icon.svg");
+	background-size: 100%;
+	background-size: cover;
+	color: white;
+}
+
+nav .nav-toggle.toggled {
+	background: url("../images/cancel-icon2.svg");
+	width: 25px;
+	height: 25px;
+	background-size: 100%;
+	background-size: cover;
+	top: 10px;
+	right: 15px;
+	color: white;
+}
+
+div.landing-hero {
+	top: 0;
+	height: 50em;
+	background-color: #ff4e00;
+	// background-image: linear-gradient(to right bottom, #ff4e00, #fc6200, #f87200, #f58100, #f18e00, #f19507, #f09c0f, #f0a317, #f3a620, #f6a927, #f8ad2e, #fbb034);
+	background-image: linear-gradient(315deg, #ff4e00 0%, #ec9f05 74%);
+	z-index: 0;
+}
+
+div.hero-filter {
+	width: 100%;
+	height: 100%;
+}
+
+div.hero-filter h2 {
+	color: black;
+	opacity: 0.7;
+	max-width: 10em;
+	font-size: 2.4em;
+	position: absolute;
+	top: 3em;
+	margin-left: 5%;
+}
+
+div.hero-filter p {
+	position: absolute;
+	top: 12em;
+	margin-left: 5%;
+	font-size: 20px;
+	width: 10em;
+	opacity: 0.7;
+}
+
+div.errors {
+	background-color: #ececec;
+	color: vars.getColor("red-alert");
+	border: 2px solid vars.getColor("red-alert");
+	border-radius: 4px;
+	min-height: 50px;
+	position: absolute;
+	left: 50%;
+	margin-right: -50%;
+	top: 100px;
+	min-width: 30em;
+	transform: translate(-50%, -50%);
+}
+
+.errors ul {
+	margin: auto;
+}
+
+form.login {
+	// font-weight: bold;
+	color: white;
+}
+
+.cancel-icon {
+	width: 30px;
+}
+
+form.login input {
+	margin-left: 2px;
+	color: white;
+	background-color: transparent;
+	border: 2px solid white;
+	border-radius: 2px;
+	outline-color: orange
+}
+
+form.login .login-btn {
+	@include vars.special-button("medium-blue", "dark-blue");
+}
+
+.hero-filter .register-btn {
+	@include vars.transparent-button;
+	display: block;
+	position: relative;
+	width: 7em;
+	top: 60%;
+	margin-left: auto;
+	margin-right: auto;
+}
+
+div.blue-background {
+	background-image: url("../images/black-circle.svg");
+	background-size: 10px;
+	background-color: #0d324d;
+}
+
+section.about-us {
+	padding: 70px 0;
+}
+
+section.about-us p {
+	max-width: 600px;
+	margin-left: auto;
+	margin-right: auto;
+	border-top: 2px solid vars.getColor("grey");
+	border-bottom: 2px solid vars.getColor("grey");
+	padding: 15px;
+	background-color: #0d324d;
+	color: white;
+}
+
+section.services-cards {
+	// margin-bottom: 0;
+	background-color: lightgrey;
+	padding-bottom: 4em;
+	background: linear-gradient(to bottom right, transparent 0%, transparent 50%, #e9e9e9 50%, vars.getColor("light-grey")  100%);
+}
+section.services-cards .cards{
+	display: flex;
+	flex-flow: wrap;
+	margin-top: 2em;
+	gap: 25px;
+	justify-content: center;
+	max-width: 1050px;
+	margin-left: auto;
+	margin-right: auto;
+	text-align: center;
+}
+
+section.services-cards h2 {
+	text-align: center;
+	padding: 10px;
+	margin-top: 0;
+	padding-top: 30px;
+	color: white;
+}
+
+section.services-cards .card {
+	width: 280px;
+	height: 350px;
+	padding: 18px 9px;
+	border-radius: 4px;
+	background: white;
+	box-shadow: 7px 10px 8px rgb(156 166 175 / 22%);
+	transition: transform 0.2s;
+	&:hover {
+		transform: translateY(-20px);
+	}
+
+}
+
+section.services-cards img {
+	width: 50px;
+	height: 50px;
+}
+
+section.services-cards button {
+	@include vars.brand-button("orange");
+}
+
+section.features-info {
+	// height: fit-content;
+	height: 70em;
+	background: radial-gradient(ellipse at left, #fff, vars.getColor("light-grey") 80%);
+}
+
+.circle-prop {
+	height: 70em;
+	overflow: hidden;
+	width: 100%;
+	position: relative;
+	// transform: translateX(-10em);
+}
+
+.circle-prop svg {
+	width: 50%;
+}
+
+.features-info img {
+	width: 600px;
+	position: absolute;
+	top: 15%;
+	right: 4%;
+}
+
+div.info-cards {
+	position: absolute;
+	top: 10em;
+	margin-left: 20%;
+	width: 70%;
+}
+
+.info-card {
+	margin-bottom: 6em;
+
+	&:nth-of-type(2) {
+		margin-left: 13%;
+	}
+
+	&:nth-of-type(3) {
+		margin-left: 10%;
+	}
+
+	&:nth-of-type(4) {
+		margin-left: 5%;
+	}
+
+	p {
+		max-width: 20em;
+		background: white;
+		padding: 5px;
+		border-radius: 4px;
+		text-align: center;
+		color: vars.getColor("faded-text2");
+		// border: 2px solid vars.getColor("medium-orange");
+		@include vars.hovering;
+
+	}
+}
+
+section.panel-infos {
+	min-height: 40em;
+	margin-top: 5em;
+	margin-bottom: 5em;
+	display: flex;
+	flex-flow: wrap;
+	align-items: center;
+	justify-content: center;
+	gap: 20px;
+
+	.panel-preview {
+		// box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
+		@include vars.hovering;
+		padding: 0;
+	}
+}
+
+.panel-infos h2 {
+	width: 100%;
+}
+
+
+.panel-info {
+	max-width: 25em;
+	margin-top: 4em;
+}
+
+.panel-info img {
+	width: 50px;
+	height: 50px;
+}
+
+.panel-info p {
+	color: vars.getColor("faded-text2");
+}
+
+section.benefits-info div {
+	margin: 1em auto;
+	padding: 1em 1em;
+	border-top: 2px solid vars.getColor("faded-text2");
+	border-bottom: 2px solid vars.getColor("faded-text2");
+	max-width: 40em;
+	color: vars.getColor("faded-text");
+}
+
+.benefits-info h2 {
+	margin-bottom: 2em;
+}
+
+section.faq-info {
+	margin-bottom: 6em;
+	margin-top: 3em;
+	min-height: 30em;
+}
+
+.faq-info h2 {
+	margin-bottom: 2em;
+}
+
+section.faq-info .collapsibles {
+	display: flex;
+	flex-flow: wrap;
+	gap: 2em;
+	justify-content: center;
+	max-width: 65em;
+	width: fit-content;
+	margin-left: auto;
+	margin-right: auto;
+	align-text: center;
+}
+
+.faq-info .collapsible {
+	text-align: center;
+	width: 20em;
+	&:not(.active) .content {
+		display: none;
+	}
+}
+
+.collapsible button {
+	width: 95%;
+	display: block;
+	margin-right: auto;
+	height: 2.5em;
+	border: 2px solid vars.getColor("brand-orange");
+	@include vars.button("light-grey");
+	font-size: 17px;
+	// font-weight: bold;
+}
+
+.collapsible button:after {
+	content: '\02795'; /* Unicode character for "plus" sign (+) */
+	font-size: 13px;
+	color: white;
+	float: right;
+	margin-left: 5px;
+}
+
+
+.collapsible .content {
+	// background-color: vars.getColor("faded-text2");
+	border: 2px solid black;
+	border-radius: 4px;
+	padding: 4px;
+	// transition: all 0.2s;
+	// height: 1px;
+}
+
+footer {
+	height: 6em;
+	background-color: vars.getColor("dark-grey");
+	padding: 20px;
+	color: vars.getColor("faded-text2");
+}
+
+footer .foot-links {
+	width: 20em;
+	margin: 2em auto;
+	text-align: center;
+}
+
+footer a {
+	margin: 0 8px;
+	color: vars.getColor("faded-text2");
+	&:hover {
+		color: vars.getColor("faded-text");
+	}
+}
+
+.hidden {
+	display: none;
+}
+
+.loading-icon {
+	display: block;
+	width: 50px;
+	height: 50px;
+	margin: auto;
+}
+
+.register-area p {
+	text-align: center;
+}
+
+.register-area .loading-icon {
+	margin-top: 100px;
+}
+
+div.register-area {
+	overflow: hidden;
+	height: 0px;
+	width: 100%;
+	z-index: 1;
+	@include vars.hovering3;
+	margin-bottom: 2em;
+	position: fixed;
+	border-radius: 0;
+	padding: 0;
+	opacity: 0;
+	transition: all 0.2s;
+	background: white;
+	h3 {
+		text-align: center;
+	}
+
+	label,input {
+		color: black;
+		display: block;
+		border-color: black;
+		width: 95%;
+	}
+
+	div {
+		margin-top: 20px;
+		margin-bottom: 20px;
+		width: 100%;
+	}
+
+	form {
+		width: 160px;
+		margin: 20px auto;
+	}
+
+	.submit-btn {
+		width: 100%;
+		background-color: #3bb78f;
+		@include vars.special-button("light-green", "dark-green");
+	}
+
+	.cancel-button {
+		width: 20px;
+		height: 20px;
+		background: url("../images/close-icon-black.svg");
+		background-size: cover;
+		position: absolute;
+		top: 20px;
+		right: 20px;
+	}
+}
+
+section.features-info {
+	// min-height: 90em;
+}
+
+div.register-area.active {
+	height: fit-content;
+	min-height: 20em;
+	opacity: 1;
+}
+
+.medium-icon {
+	width: 50px;
+	height: 50px;
+	margin-left: auto;
+	margin-right: auto;
+	display: block;
+	top: 50px;
+}
+
+// Make small screens more usable
+@media (max-width: 720px) {
+	nav { display: block; }
+
+	nav form {
+		// display: none;
+		width: 190px;
+		margin-left: auto;
+		margin-right: auto;
+		align-items: center;
+		margin-top: 5em;
+		opacity: 0;
+		transition: transform 0.3s;
+		transform: translateY(-50px);
+	}
+
+	nav form.active {
+		display: block;
+		transform: translateX(0%);
+		opacity: 1;
+		// background: white;
+		// color: black;
+	}
+
+	nav form div {
+		margin: 10px auto;
+	}
+
+	nav div label,input {
+		display: block;
+	}
+
+	nav .nav-toggle {
+		display: block;
+	}
+
+	div.register-area.active {
+		display: initial;
+	}
+
+	div.register-area h1 {
+		font-size: 1.5rem;
+		display: inline-block;
+		color: vars.getColor("brand-orange");
+	}
+
+	div.register-area h3 {
+		text-align: center;
+	}
+
+	div.register-area .login {
+		color: vars.getColor("brand-orange");
+		display: block;
+		margin-left: auto;
+		margin-right: auto;
+		// margin-top: 1em;
+		width: 10em;
+		// top: 40px;
+		// position: absolute;
+	}
+
+	div.register-area .login input {
+		border: 2px solid black;
+		border-color: black;
+		color: black;
+	}
+
+	div.register-area button {
+		margin-top: 1em;
+		width: 6em;
+	}
+
+	div.register-area div {
+		margin-top: 10px;
+		margin-bottom: 5px;
+		margin-left: auto;
+		margin-right: auto;
+	}
+
+	.landing-hero div.hero-filter h2 {
+		top: 4em;
+	}
+
+	.landing-hero div.hero-filter p {
+		top: 15em;
+	}
+
+	.landing-hero div.hero-filter .register-btn {
+		top: 75%;
+	}
+}
+
+.info-heading {
+	text-align: center;
+	margin-top: 3em;
+	margin-left: auto;
+	margin-right: auto;
+	max-width: 35em;
+	padding: 3em;
+	height: 30em;
+	button {
+		display: block;
+		margin: 3em auto;
+		width: 6em;
+		@include vars.special-button("light-green", "dark-green");
+	}
+}
+
+main.panel {
+	// min-height: 750px;
+	height: 100vh;
+	// max-height: 100vh;
+	background-image: linear-gradient(315deg, #ff4e00 0%, #ec9f05 74%);
+}
+
+#panel {
+	top: 50px;
+	padding: 0 5%;
+	margin: auto;
+	position: relative;
+	max-width: 800px;
+	height: 90%;
+	display: flex;
+	gap: 10px;
+}
+
+#sidebar {
+	position: relative;
+	width: 6%;
+	max-width: 55px;
+	height: 50%;
+	color: white;
+	background: transparent;
+	display: block;
+	flex-shrink: 0;
+
+	a {
+		display: block;
+		margin: 20px 0;
+	}
+
+	svg {
+		width: 100%;
+		height: 35px;
+		color: white;
+	}
+
+	a.selected svg {
+		color: vars.getColor("brand-orange");
+	}
+}
+
diff --git a/panel.vue b/panel.vue
new file mode 100644
index 0000000..08e866a
--- /dev/null
+++ b/panel.vue
@@ -0,0 +1,129 @@
+<template>
+<template v-if="!loading">
+<sidebar :role="user.role" :active="active"></sidebar>
+<transition name="fade" mode="out-in">
+<div v-if="active === ''" id="main">
+	<section class="welcome-pane"><h3>Welcome, {{user.name}}!</h3></section>
+	<section class="credits-pane"><img src="../../images/coin-stack.svg"
+	alt="wallet" class="icon"/><p>Credits:
+	{{(user.credits/100).toLocaleString('en')}}</p></section>
+	<section class="alerts-pane"><h4>News and Announcements</h4>
+	<p>We've just launched. Thanks for joining us! Some features are still
+	being tested.</p>
+	</section>
+	<section class="recent-pane"><h4>Recent Activity</h4>
+	<table>
+		<thead><th>Date</th><th>Name</th><th>Status</th></thead>
+		<tbody>
+		<tr v-bind:key='order.id' v-for='order in orders.slice(0, 10)'>
+		<template v-if="order.status != 'pending'">
+		<td>{{order.updated_at}}</td>
+		<td>{{order.service.name}}</td>
+		<td :class="order.status"
+		class="status"><span>{{order.status.charAt(0).toUpperCase() +
+		order.status.slice(1)}}</span></td>
+		</template>
+		</tr>
+		</tbody>
+	</table>
+	</section>
+</div>
+
+<past-orders :token="token" :orders="orders" v-else-if="active === '#orders'"
+id="main">
+</past-orders>
+
+<new-order :preferred="user.payment_method" :token="token" :active="active"
+:credits="user.credits" v-else-if="active ===
+'#new-order' || active === '#credits'" id="main" @update-user='getUser'
+@update-orders='getOrders'>
+</new-order>
+
+<div id="main" v-else-if="active === '#exit'">
+	<section class="logout-pane">
+		<h3>Are you sure you want to logout?</h3>
+		<a href="/logout">Logout</a>
+	</section>
+</div>
+
+<settings :token="token" :user="user" class="settings-page" id="main"
+v-else-if="active === '#settings'">
+</settings>
+
+<transaction-end @purchase-complete="getUser" :token="token" :user="user"
+:active="active" v-else-if="active ==
+'#transaction-complete' || active == '#transaction-failed'">
+</transaction-end>
+
+<support v-else-if="active == '#support'" :user="user" :token="token"></support>
+
+</transition>
+</template>
+</template>
+
+<script>
+import Sidebar from './sidebar.vue'
+import Settings from './settings.vue'
+import PastOrders from './orders.vue'
+import NewOrder from './services.vue'
+import TransactionEnd from './transaction-endpoint.vue'
+import Support from './support.vue'
+
+function getServices() {
+	return fetch("/panel/services", {
+		method: 'GET',
+		headers: {'Content-Type': 'application/json',
+			'Accept': 'application/json',
+			'X-XSRF-TOKEN': this.token}
+	}).then(response => {
+		response.json().then(data => {this.services = data})
+	})
+}
+
+function getUser() {
+	return fetch("/panel/user", {
+		method: 'GET',
+		headers: {'Content-Type': 'application/json',
+			'Accept': 'application/json',
+		'X-XSRF-TOKEN': this.token},
+	}).then(response => {
+		return response.json()
+	}).then(data => {
+		this.user = data
+	})
+}
+
+function getOrders() {
+	return fetch("/panel/orders", {
+		method: 'GET',
+		headers: {'Content-Type': 'application/json',
+			'Accept': 'application/json',
+		'X-XSRF-TOKEN': this.token},
+	}).then(response => {
+		return response.json()
+	}).then(data => {
+		this.orders = data
+	})
+}
+
+export default {
+	components: {
+		Sidebar, Settings, PastOrders, NewOrder,
+		TransactionEnd, Support
+	},
+	data() {
+	return {active: window.location.hash, user: null,
+	token: null, orders: null, loading: true,}
+	},
+	methods: {getUser, getServices, getOrders, },
+	created() {
+		let loaders = []
+		loaders.push(this.getUser())
+		loaders.push(this.getServices())
+		loaders.push(this.getOrders())
+		Promise.all(loaders).then(() => {
+			this.loading = false
+		})
+	}
+}
+</script>