Skip to content

Commit

Permalink
feat(api): add database support (#86)
Browse files Browse the repository at this point in the history
Co-authored-by: Romain Lanz <RomainLanz@users.noreply.github.com>

# What this PR is about?

Add database connection for vote

## If it's a new feature

- [x] `feature` Create a PHP script to add vote endpoints with ip
verification
- [x] `feature` Add MYSQL database support
- [x] `feature` Add vote on the front side
- [ ] `feature` Add env to connect the real database
- [ ] `feature` Disable vote behavior if user has already vote on the
front side

### Tests

- [x] I have run the tests of the project. `nx affected --target=test `

### Clean Code

- [x] I made sure the code is type safe (no any)

Co-authored-by: Wiliam Traoré <will@mewo.io>
Co-authored-by: bdebon <benjamin.debon@gmail.com>
  • Loading branch information
3 people authored Nov 17, 2022
1 parent c9dcdab commit 3369952
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ NEXT_PUBLIC_WEBSITE_URL="localhost:4200"
NEXT_PUBLIC_MATOMO_URL="https://choiceof.dev/matomo/"
NEXT_PUBLIC_MATOMO_SITE_ID="1"
NEXT_PUBLIC_SLUG_FOR_OFFICIAL_PREVIEW="pizza-or-instant-noodles"

NEXT_PUBLIC_API_URL="http://localhost:1234/api.php"
11 changes: 11 additions & 0 deletions .github/workflows/deploy-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
name: dist
path: dist
- uses: appleboy/scp-action@master
name: Deploying next.js build
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
Expand All @@ -34,6 +35,16 @@ jobs:
strip_components: 4
# The path is based on the directory where the user logged into the server starts
target: "~/domains/choiceof.dev/public_html"
- uses: appleboy/scp-action@master
name: Deploying php files
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
port: ${{ secrets.SSH_PORT }}
password: ${{ secrets.SSH_PASSWORD }}
source: "php/.,!node_modules"
# The path is based on the directory where the user logged into the server starts
target: "~/domains/choiceof.dev/public_html"
- name: Launch Semantic release
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
Expand Down
32 changes: 29 additions & 3 deletions apps/devchoices-next/pages/question/[slug].tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useRouter } from 'next/router'
import { questions } from '../../public/assets/data/questions'
import { useCallback, useContext, useEffect, useState } from 'react'
import { QuestionInterface } from '@benjamincode/shared/interfaces'
import { QuestionInterface, VoteInterface } from '@benjamincode/shared/interfaces'
import { PageTransitionWrapper, Question } from '@benjamincode/shared/ui'
import { QuestionContext } from '../_app'
import Image from 'next/future/image'
Expand Down Expand Up @@ -62,7 +62,16 @@ export function QuestionPage(props: QuestionPageProps) {

useEffect(() => {
// todo replace with values fetched from database
setVoteValues([Math.trunc(Math.random() * 1000), Math.trunc(Math.random() * 1000)])
fetch(`${process.env.NEXT_PUBLIC_API_URL}?slug=${slug}`)
.then((res) => res.json())
.then((data) => {
const left = data.find((v: VoteInterface) => v.position === 0) || { count: 0 }
const right = data.find((v: VoteInterface) => v.position === 1) || { count: 0 }
setVoteValues([+left.count, +right.count])
})
.catch(() => {
setVoteValues([Math.trunc(Math.random() * 1000), Math.trunc(Math.random() * 1000)])
})
}, [setVoteValues])

useEffect(() => {
Expand All @@ -86,12 +95,29 @@ export function QuestionPage(props: QuestionPageProps) {

const onLeft = () => {
// todo store the +1 in the database
const form = new FormData()
form.append('position', '0')
fetch(`${process.env.NEXT_PUBLIC_API_URL}?slug=${slug}`, {
method: 'POST',
body: form,
}).catch(() => {
setVoteValues([voteValues[0], voteValues[1] + 1])
})
setVoteValues([voteValues[0] + 1, voteValues[1]])
setShowResult(true)
}

const onRight = () => {
// todo store the +1 in the database

const form = new FormData()
form.append('position', '1')
fetch(`${process.env.NEXT_PUBLIC_API_URL}?slug=${slug}`, {
method: 'POST',
body: form,
}).catch(() => {
setVoteValues([voteValues[0], voteValues[1] + 1])
})
setVoteValues([voteValues[0], voteValues[1] + 1])
setShowResult(true)
}

Expand Down
12 changes: 12 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
db:
platform: linux/x86_64
image: mysql:8
ports:
- 127.0.0.1:3306:3306
volumes:
- mysql-data:/var/lib/mysql
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=true
volumes:
mysql-data:
5 changes: 3 additions & 2 deletions libs/shared/interfaces/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './lib/interfaces/question.interface';
export * from './lib/interfaces/choice.interface';
export * from './lib/interfaces/question.interface'
export * from './lib/interfaces/choice.interface'
export * from './lib/interfaces/vote.interface'
4 changes: 4 additions & 0 deletions libs/shared/interfaces/src/lib/interfaces/vote.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface VoteInterface {
count: string
position: number
}
66 changes: 66 additions & 0 deletions php/api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

header("Access-Control-Allow-Origin: *");

$pdo = new PDO("mysql:dbname=" . getenv('DB_NAME') . ";host=" . getenv('DB_HOST'), getenv('DB_USER'), getenv('DB_PASSWORD'));
$method = $_SERVER['REQUEST_METHOD'];
$slug = $_GET['slug'] ?? null;
$dispatchCountOn = 50;
$ip = $_SERVER["REMOTE_ADDR"];

if ($slug === null) {
die;
}

if ($method === 'GET') {
$query = $pdo->prepare('SELECT SUM(count) as count, position FROM votes WHERE slug = ? GROUP BY position');
$query->execute([$slug]);
$result = $query->fetchAll(PDO::FETCH_ASSOC);

echo json_encode($result);
return;
}

if ($method === 'POST') {
$position = $_POST['position'] ?? null;

if ($position === null) {
die;
}

$hashedIp = sha1($ip);
$query = $pdo->prepare('SELECT * FROM logs WHERE ip = ?');
$query->execute([$hashedIp]);

$result = $query->fetch(PDO::FETCH_ASSOC);

if ($result === false) {
$query = $pdo->prepare('INSERT INTO logs (ip, slugs) VALUES (?, ?);');
$query->execute([$hashedIp, json_encode([$slug])]);

insertVote($pdo, $slug, $position, $dispatchCountOn);
return;
}

$alreadyVotedFor = json_decode($result['slugs'], true);
$found = in_array($slug, $alreadyVotedFor);

if (!$found) {
array_push($alreadyVotedFor, $slug);

insertVote($pdo, $slug, $position, $dispatchCountOn);
$query = $pdo->prepare('UPDATE logs SET slugs = ? WHERE ip = ?;');
$query->execute([
json_encode($alreadyVotedFor),
$hashedIp
]);
}

return;
}

function insertVote(PDO $pdo, string $slug, int $position, int $dispatchCountOn)
{
$query = $pdo->prepare('INSERT INTO votes (slug, slot, count, position) VALUES (?, RAND() * '. $dispatchCountOn .', 1, ?) ON DUPLICATE KEY UPDATE count = count + 1;');
$query->execute([$slug, $position]);
}
12 changes: 12 additions & 0 deletions php/choiceofdev.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE votes (
slug VARCHAR(255) NOT NULL,
slot INT NOT NULL DEFAULT 0,
count INT DEFAULT NULL,
position INT NOT NULL,
UNIQUE KEY slug_slot (slug,slot,position)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE logs (
ip VARCHAR(255) NOT NULL,
slugs TEXT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

0 comments on commit 3369952

Please sign in to comment.