Skip to content

Commit 3369952

Browse files
WilliamTraoreeeWiliam Traorébdebon
authored
feat(api): add database support (#86)
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>
1 parent c9dcdab commit 3369952

File tree

8 files changed

+138
-6
lines changed

8 files changed

+138
-6
lines changed

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ NEXT_PUBLIC_WEBSITE_URL="localhost:4200"
22
NEXT_PUBLIC_MATOMO_URL="https://choiceof.dev/matomo/"
33
NEXT_PUBLIC_MATOMO_SITE_ID="1"
44
NEXT_PUBLIC_SLUG_FOR_OFFICIAL_PREVIEW="pizza-or-instant-noodles"
5-
5+
NEXT_PUBLIC_API_URL="http://localhost:1234/api.php"

.github/workflows/deploy-main.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
name: dist
2626
path: dist
2727
- uses: appleboy/scp-action@master
28+
name: Deploying next.js build
2829
with:
2930
host: ${{ secrets.SSH_HOST }}
3031
username: ${{ secrets.SSH_USER }}
@@ -34,6 +35,16 @@ jobs:
3435
strip_components: 4
3536
# The path is based on the directory where the user logged into the server starts
3637
target: "~/domains/choiceof.dev/public_html"
38+
- uses: appleboy/scp-action@master
39+
name: Deploying php files
40+
with:
41+
host: ${{ secrets.SSH_HOST }}
42+
username: ${{ secrets.SSH_USER }}
43+
port: ${{ secrets.SSH_PORT }}
44+
password: ${{ secrets.SSH_PASSWORD }}
45+
source: "php/.,!node_modules"
46+
# The path is based on the directory where the user logged into the server starts
47+
target: "~/domains/choiceof.dev/public_html"
3748
- name: Launch Semantic release
3849
env:
3950
GH_TOKEN: ${{ secrets.GH_TOKEN }}

apps/devchoices-next/pages/question/[slug].tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useRouter } from 'next/router'
22
import { questions } from '../../public/assets/data/questions'
33
import { useCallback, useContext, useEffect, useState } from 'react'
4-
import { QuestionInterface } from '@benjamincode/shared/interfaces'
4+
import { QuestionInterface, VoteInterface } from '@benjamincode/shared/interfaces'
55
import { PageTransitionWrapper, Question } from '@benjamincode/shared/ui'
66
import { QuestionContext } from '../_app'
77
import Image from 'next/future/image'
@@ -62,7 +62,16 @@ export function QuestionPage(props: QuestionPageProps) {
6262

6363
useEffect(() => {
6464
// todo replace with values fetched from database
65-
setVoteValues([Math.trunc(Math.random() * 1000), Math.trunc(Math.random() * 1000)])
65+
fetch(`${process.env.NEXT_PUBLIC_API_URL}?slug=${slug}`)
66+
.then((res) => res.json())
67+
.then((data) => {
68+
const left = data.find((v: VoteInterface) => v.position === 0) || { count: 0 }
69+
const right = data.find((v: VoteInterface) => v.position === 1) || { count: 0 }
70+
setVoteValues([+left.count, +right.count])
71+
})
72+
.catch(() => {
73+
setVoteValues([Math.trunc(Math.random() * 1000), Math.trunc(Math.random() * 1000)])
74+
})
6675
}, [setVoteValues])
6776

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

8796
const onLeft = () => {
8897
// todo store the +1 in the database
98+
const form = new FormData()
99+
form.append('position', '0')
100+
fetch(`${process.env.NEXT_PUBLIC_API_URL}?slug=${slug}`, {
101+
method: 'POST',
102+
body: form,
103+
}).catch(() => {
104+
setVoteValues([voteValues[0], voteValues[1] + 1])
105+
})
106+
setVoteValues([voteValues[0] + 1, voteValues[1]])
89107
setShowResult(true)
90108
}
91109

92110
const onRight = () => {
93111
// todo store the +1 in the database
94-
112+
const form = new FormData()
113+
form.append('position', '1')
114+
fetch(`${process.env.NEXT_PUBLIC_API_URL}?slug=${slug}`, {
115+
method: 'POST',
116+
body: form,
117+
}).catch(() => {
118+
setVoteValues([voteValues[0], voteValues[1] + 1])
119+
})
120+
setVoteValues([voteValues[0], voteValues[1] + 1])
95121
setShowResult(true)
96122
}
97123

compose.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
services:
2+
db:
3+
platform: linux/x86_64
4+
image: mysql:8
5+
ports:
6+
- 127.0.0.1:3306:3306
7+
volumes:
8+
- mysql-data:/var/lib/mysql
9+
environment:
10+
- MYSQL_ALLOW_EMPTY_PASSWORD=true
11+
volumes:
12+
mysql-data:
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
export * from './lib/interfaces/question.interface';
2-
export * from './lib/interfaces/choice.interface';
1+
export * from './lib/interfaces/question.interface'
2+
export * from './lib/interfaces/choice.interface'
3+
export * from './lib/interfaces/vote.interface'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface VoteInterface {
2+
count: string
3+
position: number
4+
}

php/api.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
header("Access-Control-Allow-Origin: *");
4+
5+
$pdo = new PDO("mysql:dbname=" . getenv('DB_NAME') . ";host=" . getenv('DB_HOST'), getenv('DB_USER'), getenv('DB_PASSWORD'));
6+
$method = $_SERVER['REQUEST_METHOD'];
7+
$slug = $_GET['slug'] ?? null;
8+
$dispatchCountOn = 50;
9+
$ip = $_SERVER["REMOTE_ADDR"];
10+
11+
if ($slug === null) {
12+
die;
13+
}
14+
15+
if ($method === 'GET') {
16+
$query = $pdo->prepare('SELECT SUM(count) as count, position FROM votes WHERE slug = ? GROUP BY position');
17+
$query->execute([$slug]);
18+
$result = $query->fetchAll(PDO::FETCH_ASSOC);
19+
20+
echo json_encode($result);
21+
return;
22+
}
23+
24+
if ($method === 'POST') {
25+
$position = $_POST['position'] ?? null;
26+
27+
if ($position === null) {
28+
die;
29+
}
30+
31+
$hashedIp = sha1($ip);
32+
$query = $pdo->prepare('SELECT * FROM logs WHERE ip = ?');
33+
$query->execute([$hashedIp]);
34+
35+
$result = $query->fetch(PDO::FETCH_ASSOC);
36+
37+
if ($result === false) {
38+
$query = $pdo->prepare('INSERT INTO logs (ip, slugs) VALUES (?, ?);');
39+
$query->execute([$hashedIp, json_encode([$slug])]);
40+
41+
insertVote($pdo, $slug, $position, $dispatchCountOn);
42+
return;
43+
}
44+
45+
$alreadyVotedFor = json_decode($result['slugs'], true);
46+
$found = in_array($slug, $alreadyVotedFor);
47+
48+
if (!$found) {
49+
array_push($alreadyVotedFor, $slug);
50+
51+
insertVote($pdo, $slug, $position, $dispatchCountOn);
52+
$query = $pdo->prepare('UPDATE logs SET slugs = ? WHERE ip = ?;');
53+
$query->execute([
54+
json_encode($alreadyVotedFor),
55+
$hashedIp
56+
]);
57+
}
58+
59+
return;
60+
}
61+
62+
function insertVote(PDO $pdo, string $slug, int $position, int $dispatchCountOn)
63+
{
64+
$query = $pdo->prepare('INSERT INTO votes (slug, slot, count, position) VALUES (?, RAND() * '. $dispatchCountOn .', 1, ?) ON DUPLICATE KEY UPDATE count = count + 1;');
65+
$query->execute([$slug, $position]);
66+
}

php/choiceofdev.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CREATE TABLE votes (
2+
slug VARCHAR(255) NOT NULL,
3+
slot INT NOT NULL DEFAULT 0,
4+
count INT DEFAULT NULL,
5+
position INT NOT NULL,
6+
UNIQUE KEY slug_slot (slug,slot,position)
7+
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
8+
9+
CREATE TABLE logs (
10+
ip VARCHAR(255) NOT NULL,
11+
slugs TEXT NOT NULL
12+
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

0 commit comments

Comments
 (0)