Ethosa
Posted on August 11, 2023
Get Started
In this article we'll use these technologies:
- Nim programming language šØ
- HappyX web framework š“
- Tailwind CSS 3 āØ
- SQLite as database ā
To make real-time voting application we need websockets. Websockets is mostly useful to give real-time
features to your program.
HappyX web framework provides working with websockets so we need install only Nim and HappyX. š”
Install
You can install Nim with two ways:
In this article we'll use choosenim.
Enter this command and follow instructions
wget -qO - https://nim-lang.org/choosenim/init.sh | sh
Next, we choose Nim v2.0.0
choosenim 2.0.0
Now we need to install HappyX web framework. We can do it with nimble
package manager:
nimble install happyx@#head
Create Project
To create project just use these commands
mkdir vote_app
cd vote_app
hpx create --kind:SPA --name:client --use-tailwind
hpx create --kind:SSR --name:server
Next, open š vote_app/server/src/main.nim
. Let's write some procedures to work with database. Final version of main.nim
:
# Import HappyX
import
happyx, # happyx web framework
db_sqlite # stdlib Sqlite
proc initDataBase(): DbConn =
## Creates Database connection.
let res = open("votes.db", "", "", "")
# Create users table
res.exec(sql"""CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login TEXT NOT NULL,
pswd TEXT NOT NULL
);""")
# Create votes table
res.exec(sql"""CREATE TABLE IF NOT EXISTS vote (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL,
pollId INTEGER NOT NULL,
answerId INTEGER NOT NULL
);""")
# Create poll table
res.exec(sql"""CREATE TABLE IF NOT EXISTS poll (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL
);""")
# Create poll answer table
res.exec(sql"""CREATE TABLE IF NOT EXISTS answer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pollId INTEGER NOT NULL,
title TEXT NOT NULL
);""")
res
proc users(db: DbConn): seq[Row] =
## Retrieves all created users
db.getAllRows(sql"SELECT * FROM user")
proc votes(db: DbConn): seq[Row] =
## Retrieves all created votes
db.getAllRows(sql"SELECT * FROM vote")
proc answers(db: DbConn): seq[Row] =
## Retrieves all created answers
db.getAllRows(sql"SELECT * FROM answer")
proc polls(db: DbConn): seq[Row] =
## Retrieves all created polls
db.getAllRows(sql"SELECT * FROM poll")
# Serve at http://127.0.0.1:5123
serve "127.0.0.1", 5123:
# Connect to Database
let db = initDataBase()
# on GET HTTP method at http://127.0.0.1:5123/
get "/":
# Respond plain text
"Hello, world!"
Now, let's write the frontend part. Go to š vote_app/client/
and enter command:
hpx dev --reload
This command executes our single-page application and automatically opens the browser. Flag --reload
enables hot code reloading š„.
Next, let's rewrite HelloWorld
component. Open š vote_app/client/src/components/hello_world.nim
. Rename it to header.nim
. Final version of the file:
# Import HappyX
import happyx
# Declare component
component Header:
# Declare HTML template
`template`:
tDiv(class = "flex items-center justify-center w-fill sticky top-0 font-mono text-sm font-bold px-8 py-2 bg-purple-200"):
tP(class = "scale-75 select-none cursor-pointer"):
"ā
real-time voting app ā"
š vote_app/client/src/main.nim
:
# Import HappyX
import
happyx,
components/[header]
# Declare application with ID "app"
appRoutes("app"):
"/":
# Component usage
component Header
So we got this
ā (webpage is
200%
scaled)
Next step is authorization.
Authorization
Users must be authorized to vote. So first we will add POST
method to sing up and GET
method for login
Backend
Go to š vote_app/server/src/main.nim
and write additional procedures:
proc userExists(db: DbConn, username: string): bool =
## Returns true if user is exists
for user in db.users():
if user[1] == username:
return true
false
proc getUser(db: DbConn, login, password: string): Row =
## get user by password and login
for user in db.users():
# Compare user login and user password hash
if user[1] == login and check_password(password, user[2]):
return user
Row(@[])
Next step is to declare the request model for POST
method:
# Declare Auth request model to user registration
model Auth:
username: string
password: string
Authorization mount š
mount Authorization:
get "/sign-in[auth:Auth]":
## Authorizes user if available.
##
## On incorrect data responds 404.
## On success returns user's ID
var user = db.getUser(query~username, query~password)
if user.len == 0:
statusCode = 404
return {"response": "failure"}
else:
return {"response": parseInt(user[0])}
post "/sign-up[auth:Auth]":
## Registers user if available.
##
## When username is exists responds 404
if db.userExists(auth.username):
statusCode = 404
return {"response": fmt"failure. user {auth.username} is exists."}
else:
db.exec(
sql"INSERT INTO user (login, pswd) VALUES (?, ?)",
auth.username, generate_password(auth.password)
)
return {"response": "success"}
The final step is to use mount in our server š”
# Setup CORS
regCORS:
origins: ["*"]
headers: ["*"]
methods: ["*"]
credentials: true
# Serve at http://127.0.0.1:5123
serve "127.0.0.1", 5123:
# Connect to Database
let db = initDataBase()
# on GET HTTP method at http://127.0.0.1:5123/auth/...
mount "/auth" -> Authorization
Let's start our app:
cd vote_app/server/src
nim c -r main.nim
User registration [POST] | User authorization [GET] |
---|---|
Frontend
On the frontend side we will create a small reg/auth form. Let's create a file vote_app/client/src/components/auth.nim
:
import happyx
component Authorization:
# Callback that can be notified on auth is success š
callback: (proc(authorized: bool): void) = (proc(authorized: bool) = discard)
`template`:
tDiv(class = "flex flex-col px-6 py-4 items-center rounded-md drop-shadow-2xl bg-purple-100 gap-8"):
tP(class = "font-mono font-bold"):
"authorization š"
tDiv(class = "flex flex-col gap-2"):
tInput(id = "login", placeholder = "Username ...", class = "text-center rounded-md px-4 font-mono")
tInput(id = "password", `type` = "password", placeholder = "Password ...", class = "font-mono text-center rounded-md px-4")
tDiv(class = "flex justify-center items-center w-full justify-around"):
tButton(class = "bg-none rounded-md px-2 text-sm font-mono border-2 border-purple-600 hover:border-purple-700 active:border-purple-800 transition-all"):
"sign in"
@click:
# Try to authorize
let
inpLogin = document.getElementById("login")
inpPassword = document.getElementById("password")
auth(self.Authorization, inpLogin.value, inpPassword.value)
[methods]:
proc auth(username, password: cstring) =
buildJs:
function handleResponse(response):
# Handle authorization response
console.log(response)
console.log(response.response)
if typeof response.response == "number":
nim:
self.callback.val()(true)
else:
nim:
self.callback.val()(false)
fetch("http://localhost:5123/auth/sign-in?username=" + ~username + "&password=" + ~password).then(
(e) => e.json().then(
(response) => handleResponse(response)
)
)
Let's use our component āØ
vote_app/client/src/main.nim
:
# Import HappyX
import
happyx,
components/[header, auth]
# Declare application with ID "app"
appRoutes("app"):
"/":
# Component usage
component Header
tDiv(class = "absolute top-0 bottom-0 left-0 right-0 flex flex-col justify-center items-center"):
component Authorization
So we got this authorization window
Let's show polls on user authorized āØ
First we'll add GET
method that will respond polls data on the server
get "/polls":
var polls = newJArray()
for poll in db.polls():
polls.add(%*{
"id": poll[0],
"title": poll[1],
"description": poll[2]
})
return {
"response": polls
}
Create polls.nim
file into š vote_app/client/src/components
.
Add the code snippet below:
import happyx
component Polls:
data: seq[tuple[
i: int,
t, d: cstring,
answers: seq[tuple[i, pId: int, t: cstring]]
]] = @[]
`template`:
tDiv(class = "flex flex-col gap-4 w-full h-full justify-center items-center px-8"):
for poll in self.data:
tDiv(class = "w-full rounded-md bg-purple-100 drop-shadow-xl px-8 py-2"):
tP(class = "font-mono font-bold lowercase"):
{poll.t}
if poll.d.len > 0:
tP(class = "font-mono text-sm opacity-75 lowercase"):
{poll.d}
tDiv(class = "flex flex-col gap-2"):
for answer in poll.answers:
tDiv(class = "flex font-mono lowercase font-sm justify-center items-center rounded-md bg-purple-50 select-none cursor-pointer"):
{answer.t}
@created:
self.loadPolls()
[methods]:
proc loadPolls() =
# Disable renderer at few time
enableRouting = false
# Write pure JavaScript with Nim syntax
buildJs:
function foreach(data):
# Declare nim variables
nim:
var
id: int
title: cstring
description: cstring
answers: seq[tuple[i, pId: int, t: cstring]] = @[]
# Load data from JS
~id = data.id
~title = data.title
~description = data.description
# Load answers from JS And add in Nim
for answer in data.answers:
nim:
var
answerId: int
pollId: int
answerTitle: cstring
~answerId = answer.id
~pollId = answer.pollId
~answerTitle = answer.title
nim:
# Load JS data to Nim
answers.add((answerId, pollId, answerTitle))
nim:
self.data->add((id, title, description, answers))
function handlePolls(response):
# Handle response
response.forEach(e => foreach(e))
nim:
enableRouting = true
# Rerender our app
application.router()
# Fetch data from our API
fetch("http://localhost:5123/polls").then(e => e.json().then(
x => handlePolls(x.response)
))
Next step is component usage. Let's rewrite vote_app/client/src/main.nim
:
# Import HappyX
import
happyx,
components/[header, auth, polls]
var authState = remember false
proc handleAuth(authorized: bool) =
authState.set(authorized)
# Declare application with ID "app"
appRoutes("app"):
"/":
# Component usage
component Header
tDiv(class = "absolute top-0 bottom-0 left-0 right-0 flex flex-col justify-center items-center"):
if not authState:
component Authorization(callback = handleAuth)
else:
component Polls
And we got this after authorization:
See you soon! š
Posted on August 11, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.