29
loading...
This website collects cookies to deliver better user experience
(ql:quicklisp :caveman2)
(:caveman2:make-project #P"~/quicklisp/local-projects/super-rentals" :author "Rajasegar")
templates/layouts/
called default.html
.<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" type="text/css" media="screen" href="/css/main.css">
</head>
<body>
<div class="container">
<nav class="menu">
<a href="/"> <h1>Super Rentals</h1></a>
<div class="links">
<a href="/about">About</a>
<a href="/contact">Contact</a>
</div>
</nav>
<div class="body">
{% block content %}{% endblock %}
</div>
</div>
</body>
</html>
{% block title %}
and {% block content %}
. These are the placeholders in which we override the content for each individual pages. The title
placeholder is to give a provision to update the page title for individual pages and the content
placeholder for the respective content for the pages.tags
in Djula. Tags are used to create text in the output, some control flow by performing loops or logic, and some load external information into the template to be used by later variables.index.html
in the templates
folder. This template is mapped to our default route /
or index page in our website. The mappings are created in a separate Lisp file called web.lisp
inside the src
folder.(in-package :cl-user)
(defpackage super-rentals.web
(:use :cl
:caveman2
:super-rentals.config
:super-rentals.view
:super-rentals.db
:datafly
:sxql)
(:export :*web*))
(in-package :super-rentals.web)
;; for @route annotation
(syntax:use-syntax :annot)
;;
;; Application
(defclass <web> (<app>) ())
(defvar *web* (make-instance '<web>))
(clear-routing-rules *web*)
;;
;; Routing rules
(defroute "/" ()
(render #P"index.html" ))
;;
;; Error pages
(defmethod on-exception ((app <web>) (code (eql 404)))
(declare (ignore app))
(merge-pathnames #P"_errors/404.html"
*template-directory*))
templates
directory as index.html
, we just need to add the respective HTML in the template.title
and content
blocks as per our liking.{% extends "layouts/default.html" %}
{% block title %}Home - Super Rentals{% endblock %}
{% block content %}
<div id="main">
<div class="jumbo">
<div class="right"></div>
<h2>Welcome to Super Rentals!</h2>
<p>We hope you find exactly what you're looking for in a place to stay.</p><a class="button" href="/about">About Us</a>
</div>
...
</div>
<div class="rentals">
<label>
<span>Where would you like to stay?</span>
<input class="light" type="text" name="search" hx-post="/search" hx-trigger="keyup changed delay:500ms" hx-target=".results" hx-indicator=".htmx-indicator">
</label>
<div class="htmx-indicator"> Searching rentals...</div>
</div>
hx-
. This is for using a special JS library called htmx to send AJAX requests from HTML itself without writing any boilerplate JavaScript code. keyup
event of the input element with a delay of 500ms
we are sending a POST
HTTP request to the url endpoint <http://locahost:3000/search>
and put the response of the AJAX request into an element with class name .results
, which is in our case, an unordered list (ul) element having the list of rentals and we are showing the progress of the request with a special element with a class called .htmx-indicator
. Hence while the request in progress, the Search rentals…
will be displayed and once the request is complete it is hidden automatically by the htmx library.<script src="https://unpkg.com/[email protected]"></script>
<div class="rentals">
...
<ul class="results">
{% for rental in rentals %}
<li>
<article class="rental">
<button class="image" type="button" _="on click toggle .large then if #view-caption.textContent === 'View Larger' then set #view-caption.textContent to 'View Smaller' else set #view-caption.textContent to 'View Larger'">
<img src="{{rental.image}}" alt="An image of {{rental.title}}">
<small id="view-caption">View Larger</small>
</button>
<div class="details">
<h3><a href="/rentals/{{rental.id}}">{{rental.title}}</a></h3>
<div class="detail owner"><span>Owner: </span>{{rental.owner}}</div>
<div class="detail type"><span>Type: </span>{{rental.category}}</div>
<div class="detail location"><span>Location: </span>{{rental.city}}</div>
<div class="detail bedrooms"><span>Bedrooms: </span>{{rental.bedrooms}}</div>
</div>
<div class="map">
<img alt="A map of {{rental.title}}" src="https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/{{rental.lng}},{{rental.lat}},9/150x150@2x?access_token=pk.eyJ1IjoicmFqYXNlZ2FyYyIsImEiOiJja2w2MzV0M2MyZHJnMzBtczA3ODJsOWZ2In0.pwUodXBD7MxNMF38fs0UsQ" width="150" height="150">
</div>
</article>
</li>
{% endfor %}
</ul>
</div>
_
attribute, which is actually hyperscript to toggle the image to smaller and larger sizes.<button class="image" type="button" _="on click toggle .large then if #view-caption.textContent === 'View Larger' then set #view-caption.textContent to 'View Smaller' else set #view-caption.textContent to 'View Larger'">
view-caption
to toggle between View Larger
and View Smaller
values.src/web.lisp
. But before that we need to define the structure of our rentals listing data.(defvar *rentals* '(("grand-old-mansion" . (("id" . "grand-old-mansion")
("title" . "Grand Old Mansion")
("owner" . "John McCarthy")
("city" . "San Francisco")
("lat" . "37.7749")
("lng" . "-122.4194")
("category" . "Estate")
("bedrooms" . "15")
("image" . "https://upload.wikimedia.org/wikipedia/commons/c/cb/Crane_estate_(5).jpg")
("description" . "This grand old mansion sits on over 100 acres of rolling hills and dense redwood forests.")))
("urban-living" . (("id" . "urban-living")
("title" . "Urban Living")
("owner" . "Paul Graham")
("city" . "Seattle")
("lat" . "47.6062")
("lng" . "-122.3321")
("category" . "Condo")
("bedrooms" . "1")
("image" . "https://upload.wikimedia.org/wikipedia/commons/2/20/Seattle_-_Barnes_and_Bell_Buildings.jpg")
("description" . "A commuters dream. This rental is within walking distance of 2 bus stops and the Metro.")))
("downtown-charm" . (("id" . "downtown-charm")
("title" . "Downtown Charm")
("owner" . "Guy Steele")
("city" . "Portland")
("lat" . "45.5175")
("lng" . "-122.6801")
("category" . "Apartment")
("bedrooms" . "3")
("image" . "https://upload.wikimedia.org/wikipedia/commons/f/f7/Wheeldon_Apartment_Building_-_Portland_Oregon.jpg")
("description" . "Convenience is at your doorstep with this charming downtown rental. Great restaurants and active night life are within a few feet.")))))
*rentals*
to our index template via the route. We should change our index route definition to something like this.(defroute "/" ()
(render #P"index.html" (list :rentals (mapcar #'(lambda (r) (cdr r)) *rentals*))))
*rentals*
list to only have the values sent to the template not the keys. For that we are making use of the mapcar
function available in Lisp.main.css
located in the static/css
folder at the root of the project folder. The below are some styles for the rental listing web site , you can refer to the full css file at Github.rental h3 a {
display: inline;
}
.rental .detail {
flex-basis: 50%;
font-weight: 300;
font-style: italic;
white-space: nowrap;
}
.rental .detail span {
font-weight: 400;
font-style: normal;
}
src/web.lisp
.(defroute "/about" ()
(render #P"about.html"))
about.html
inside the templates
folder with the following content. Here also we are extending the default layout and overriding the title
and content
placeholders.{% extends "layouts/default.html" %}
{% block title %}About - Super Rentals{% endblock %}
{% block content %}
<div id="main">
<div class="jumbo">
<div class="right"></div>
<h2>About Super Rentals</h2>
<p>The Super Rentals website is a delightful project created to explore <a href="https://lisp-lang.org">Common Lisp</a> web development. By building a property rental site, we can simultaneously imagine traveling and building Common Lisp web applications.</p><a class="button" href="/contact">Contact Us</a></div>
</div>
{% endblock %}
<http://localhost:3000/rentals/:id>
where id is the uniqued identifier for the rental listing. So for example the url http://localhost:3000/rentals/urban-living
will point to a specific rental listing known as Urban Living
.:id
to capture the id and then we will pick the rental based on that id from the *rentals*
list using the assoc
function.(defroute "/rentals/:id" (&key id)
(render #P"rentals.html"
(list :rental (cdr (assoc id *rentals* :test #'string=)))))
{{rental}}
And we can access its properties using the dot notation like {{rental.title}}
{% extends "layouts/default.html" %}
{% block title %}Rentals - Super Rentals{% endblock %}
{% block content %}
<div id="main">
<div class="jumbo">
<div class="right tomster"></div>
<h2>{{rental.title}}</h2>
<p>Nice find! This looks like a nice place to stay near San Francisco.</p>
<a class="button share" href="#">Share on Twitter</a></div>
<article class="rental detailed">
<button class="image" type="button" _="on click toggle .large then if #view-caption.textContent === 'View Larger' then set #view-caption.textContent to 'View Smaller' else set #view-caption.textContent to 'View Larger'">
<img src="{{rental.image}}" alt="An image of Grand Old Mansion">
<small id="view-caption">View Larger</small>
</button>
<div class="details">
<h3>{{rental.title}}</h3>
<div class="detail owner"><span>Owner: </span>{{rental.owner}}</div>
<div class="detail type"><span>Type: </span>{{rental.category}}</div>
<div class="detail location"><span>Location: </span>{{rental.city}}</div>
<div class="detail bedrooms"><span>Bedrooms: </span>{{rental.bedrooms}}</div>
<div class="detail description">
<p>{{rental.description}}</p>
</div>
</div>
<div class="map"><img class="large" alt="A map of Grand Old Mansion" src="https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/{{rental.lng}},{{rental.lat}},12/894x600@2x?access_token=pk.eyJ1IjoicmFqYXNlZ2FyYyIsImEiOiJja2w2MzV0M2MyZHJnMzBtczA3ODJsOWZ2In0.pwUodXBD7MxNMF38fs0UsQ" width="894" height="600"></div>
</article>
</div>
{% endblock %}
defroute
function in src/web.lisp
along with the other route definitions. Here we need to specify the type of the HTTP method our route will handle, since our search routes handles POST
methods, we need to supply the :method
keyword with the value :POST
. &key _parsed
parameter within the route definition to find our body params that has been sent along with the POST
request.search
parameter this is how we will extract from the _parsed
key:(cdr (assoc "search" _parsed :test #'string=))
POST
parameters are parsed in the form of an association list or alist like the below in Caveman, we need to use the assoc
function to extract the value.(("search" . "urban"))
filter-rentals
and pass the query value as the argument for the same. We also need to send only the values of the *rentals*
list to the template, not the keys, so that they can be iterated properly and the rental details are displayed properly in the HTML.(defroute ("/search" :method :POST) (&key _parsed)
(let ((query (cdr (assoc "search" _parsed :test #'string=))))
(render #P"search.html"
(list :rentals
(mapcar #'(lambda (rental)
(cdr rental)) (filter-rentals query))))))
*rentals*
list, we are making use of the mapcar
function in Lisp to transform the list.(mapcar #'(lambda (rental) (cdr rental)) *rentals*)
*rentals*
list.filter-rentals
to filter only the rentals that match the title with the query string supplied. So we are iterating the *rentals*
list here, then extracting the title of each rental and matching the query string using the search
function in Lisp, and returning nil if the search is found, because we want to keep the rental in the list, because we are manipulating the list using the remove-if
function. Otherwise we return T
so that the not-matching rental will be removed from the new list.filter-rentals
function.(defun filter-rentals (query)
"Filter the rentals not matching query string"
(remove-if #'(lambda (rental)
(let ((title (cdr (assoc "title" (cdr rental) :test #'string=))))
(if (search query title :test #'char-equal)
nil
t))) *rentals* ))
{% for %}
tag available in Djula.{% for rental in rentals %}
<li>
<article class="rental">
<button class="image" type="button" _="on click toggle .large then if #view-caption.textContent === 'View Larger' then set #view-caption.textContent to 'View Smaller' else set #view-caption.textContent to 'View Larger'">
<img src="{{rental.image}}" alt="An image of {{rental.title}}">
<small id="view-caption">View Larger</small>
</button>
<div class="details">
<h3><a href="/rentals/{{rental.id}}">{{rental.title}}</a></h3>
<div class="detail owner"><span>Owner: </span>{{rental.owner}}</div>
<div class="detail type"><span>Type: </span>{{rental.category}}</div>
<div class="detail location"><span>Location: </span>{{rental.city}}</div>
<div class="detail bedrooms"><span>Bedrooms: </span>{{rental.bedrooms}}</div>
</div>
<div class="map">
<img alt="A map of {{rental.title}}" src="https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/{{rental.lng}},{{rental.lat}},9/150x150@2x?access_token=pk.eyJ1IjoicmFqYXNlZ2FyYyIsImEiOiJja2w2MzV0M2MyZHJnMzBtczA3ODJsOWZ2In0.pwUodXBD7MxNMF38fs0UsQ" width="150" height="150">
</div>
</article>
</li>
{% endfor %}
heroku apps:create cl-super-rentals --buildpack https://github.com/gos-k/heroku-buildpack-roswell
cl-super-rentals
won't work for you, because it has already been take for this tutorial, so please use a different name for your app.git push heroku master
29