Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
Stephen
Admin Panel
Commits
6ad46e77
Commit
6ad46e77
authored
Apr 20, 2020
by
Stephen
💯
Browse files
Add all of HTML to the admin panel
parent
f61a131c
Changes
4
Hide whitespace changes
Inline
Side-by-side
README2.md
View file @
6ad46e77
# Building An Admin Panel
Now that you have completed the previous
[
Hello World
](
https://kado.org/guide/hello-world/
)
,
[
Make a Simple Website
](
https://kado.org/guide/make-simple-website/
)
and
[
Database Work Flows
](
https://kado.org/guide/database-work-flow/
)
guides, It is
important to note that all the guides combine together into a single project
we will be moving on to building an admin panel.The first step we will be doing
is creating a route folder and within that route folder we will create a
admin.js file and you will use the following code:
`Admin.js`
.
```
js
'
use strict
'
const
fs
=
require
(
'
kado/lib/FileSystem
'
)
class
Admin
{
static
register
(
app
)
{
const
route
=
new
Admin
(
fs
.
path
.
join
(
__dirname
,
'
../view/admin
'
))
app
.
get
(
'
/admin/
'
,
route
.
index
())
app
.
get
(
'
/admin/login/
'
,
route
.
login
())
app
.
post
(
'
/admin/login/
'
,
route
.
login
())
app
.
get
(
'
/admin/logout/
'
,
route
.
logout
())
}
```
The code above uses
`fs(filesystem)`
require in order to get the
`(‘kado/lib/FileSystem’)`
then we use the class function to call the admin object.
Then we use static register to acquire the app. The next line
const route = new Admin makes an instance of our route object, then placing
`fs.path.join`
method to create and join the path in a string. Lastly, we use
the
`_dirname`
variable to tell us the absolute path of the directory containing
the executing file, then using app.get() for matching and handling a specific
route when requested.
Beneath the code above you’ll be using the following code:
`Admin.js`
.
```
js
index
()
{
return
(
req
,
res
)
=>
{
if
(
!
req
.
session
.
get
(
'
staff
'
))
{
return
res
.
render
(
'
admin/error
'
,
{
error
:
'
Must be logged in
'
})
}
req
.
locals
.
_pageTitle
=
'
Home
'
res
.
render
(
'
admin/home
'
)
}
}
```
The above code is for creating an index and in that we use return to get our
request and response. Then using
`if(!req.session.get(‘staff’)) {`
when we
successfully get the session we will return and render a response
`(‘admin/error’, { error: ‘Must be logged in’ })`
. On the last two lines the
`pageTitle`
is set and we render the page using
`res.render(‘admin/home’)`
.
Next, we will add a login method to handle displaying and authenticating staff
members. Adding on to Admin.js:
`Admin.js`
.
```
js
login
()
{
return
(
req
,
res
)
=>
{
if
(
req
.
body
.
email
)
{
req
.
session
.
set
(
'
staff
'
,
{
email
:
req
.
body
.
email
})
res
.
statusCode
=
302
let
referer
=
req
.
headers
.
referer
||
'
/admin/
'
if
(
referer
.
match
(
/
\/
admin
\/
login
\/
$/
))
referer
=
'
/admin/
'
res
.
setHeader
(
'
Location
'
,
referer
)
return
res
.
end
()
}
req
.
locals
.
_pageTitle
=
'
Login
'
res
.
render
(
'
admin/login
'
)
}
}
```
We will use
`login()`
and by doing so we are creating the login. Under that we
will return the
`req`
and
`res`
, then using
`if(req.body.email)`
we will get the
email body and next we will set the session so that it's for staff and get back
the email body and respond the
`StatusCode 302`
. The next line we use referer
the headers is the
`‘/admin/’`
and then match the referer with the admin and
login and then get the
`setHeader(‘location’, referer)`
.
Finally place this section of code to handle staff logging out of the system:
`Admin.js`
.
```
js
logout
()
{
return
(
req
,
res
)
=>
{
req
.
session
.
set
(
'
staff
'
,
undefined
)
res
.
statusCode
=
302
res
.
setHeader
(
'
Location
'
,
'
/admin/login/
'
)
return
res
.
end
()
}
}
}
```
This will create the logout feature. The next line of code we route the function
and set the session for staff and its undefined because it has been declared but
not assigned a value. Then we get the
`statusCode`
and that will be
`302`
and
the set the header and end it.
Then you will use the following code to represent the current module and exports
as a module:
`Admin.js`
.
```
js
module
.
exports
=
Admin
```
Now that we have completed the admin.js we will be creating a
`Help.js`
file
and with in the Help.js file we will be using the following code:
`Help.js`
.
```
js
'
use strict
'
const
Assert
=
require
(
'
kado/lib/Assert
'
)
const
fs
=
require
(
'
kado/lib/FileSystem
'
)
const
HelpModel
=
require
(
'
../model/HelpModel
'
)
const
Module
=
require
(
'
kado/lib/Module
'
)
const
Route
=
require
(
'
../lib/Route
'
)
class
Help
extends
Module
{
constructor
()
{
super
()
this
.
app
=
null
this
.
name
=
'
help
'
this
.
title
=
'
Help
'
this
.
description
=
'
Manage help articles
'
}
```
The code above we use Assert, fs, HelpModel, Module and Route to require and
connect the listed folder and files. Next we use the class Help extends module
to create the child class of another class, then we use the constructor()
function that initializes an object, then using the super class to call the
constructor to access the parent properties and methods which will be the
this.app, .name, .title, and .description.
The next section of code we will use the following:
`Help.js`
.
```
js
list
()
{
return
async
(
req
,
res
)
=>
{
const
db
=
this
.
db
.
getEngine
()
const
query
=
HelpModel
.
list
()
const
rv
=
await
Route
.
listRoute
(
db
,
query
)
req
.
locals
.
_pagetitle
=
'
List Help Articles
'
res
.
render
(
'
admin/help/list
'
,
{
rows
:
rv
[
0
],
fields
:
rv
[
1
]
})
}
}
```
The above code we will create a
`list()`
and we are doing that because we can use
this to store multiple pieces of information at once, such as a list of records
in the database or a list of contents. On the next line we use the return async
function to create a promise that will be resolved with the returned value.
Then we use const db to get the database engine and
`HelpModel`
is used to obtain
a list query from the model to pass that query to the database engine, then we
set the page title with
`req.locals_pageTitle`
, then we pass the return values
from the database to the template for rendering.
Use the following for the the next section of code:
`Help.js`
.
```
js
listAction
()
{
return
async
(
req
,
res
)
=>
{
const
db
=
this
.
db
.
getEngine
()
const
query
=
HelpModel
.
delete
()
const
rv
=
await
Route
.
listActionRoute
(
db
,
query
,
req
.
body
)
if
(
rv
.
deleted
)
res
.
setHeader
(
'
X-Deleted
'
,
rv
.
deleted
)
res
.
redirect
(
'
/admin/help/
'
)
}
}
```
Now we will be using the list action method which will be similar to the
previous code we used. We are now using the list action method and the
difference is that we are using the delete method and that will use the
delete query. The next line we are getting the route to the listAction to go to
the db, query and
`req.body`
. The next line we are saying if the variable is
deleted we get the setHeader response and redirect to
`(‘/admin/help’)`
.
Moving on to the next section of code you will be using is the following:
`Help.js`
.
```
js
create
()
{
return
(
req
,
res
)
=>
{
req
.
locals
.
_pagetitle
=
'
Create Help Article
'
res
.
render
(
'
admin/help/create
'
)
}
}
```
The next line we use
`create()`
so that we use the object to set the page title
and use the create Help article and then render the
`(‘admin/help/create’)`
.
Under the above code you will now be using the following:
`Help.js`
.
```
js
edit
()
{
return
async
(
req
,
res
)
=>
{
const
db
=
this
.
db
.
getEngine
()
const
query
=
HelpModel
.
byId
(
req
.
query
.
get
(
'
id
'
))
const
rv
=
await
db
.
execute
(
query
.
toString
(),
query
.
toArray
())
Assert
.
isType
(
'
Array
'
,
rv
[
0
])
const
help
=
new
HelpModel
(
rv
[
0
].
shift
())
res
.
render
(
'
admin/help/edit
'
,
{
help
:
help
})
}
}
```
Now we will be using the
`edit()`
in order to edit the db, query and rv. First
we get the database engine. Then we call the query to the
`HelpModel.byid`
to
get the id query, next we execute the database query to a string and an array.
The next line we have
`Assert.isType`
which will get us the array and get the
new
`HelpModel`
value. Lase we rent the
`(‘admin/help/edit’, { help: help })`
.
Use the following to create the
`save()`
:
`Help.js`
.
```
js
save
()
{
return
async
(
req
,
res
)
=>
{
const
db
=
this
.
db
.
getEngine
()
const
id
=
req
.
body
.
id
||
0
await
Route
.
saveRoute
(
db
,
HelpModel
,
id
,
req
.
body
)
res
.
redirect
(
'
/admin/help/
'
)
}
}
```
The next part of code is the save which pretty much has a lot of the similar
code except this one will save the route for our db, HelpModel, id, and
req.body.
The next section of code you will use the following:
`Help.js`
.
```
js
admin
(
app
)
{
this
.
app
=
app
this
.
db
=
this
.
app
.
database
.
getEngine
(
'
mysql
'
)
app
.
get
(
'
/admin/help/
'
,
this
.
list
())
app
.
get
(
'
/admin/help/create/
'
,
this
.
create
())
app
.
get
(
'
/admin/help/edit/
'
,
this
.
edit
())
app
.
post
(
'
/admin/help/
'
,
this
.
listAction
())
app
.
post
(
'
/admin/help/save/
'
,
this
.
save
())
}
```
With this part of code we have the
`admin()`
and that will initiate the app and
get the database engine and connect the following using the app.get.
Now we will be using the following code for the next section:
`Help.js`
.
```
js
main
(
app
)
{
// need routes
app
.
get
(
'
/help/
'
,
(
req
,
res
)
=>
{
res
.
render
(
fs
.
path
.
join
(
__dirname
,
'
/view/help.html
'
))
})
}
}
```
The code above will have the
`main(app)`
and you will use app.get for
`(‘/help/’, (req, res)`
and render the filesystem and join the path to the
`(_dirname, ‘/view/help/html’))`
.
Finally the last part of code we have is the last part you will need for the
same reason as we talked about above:
`Help.js`
.
```
js
Help
.
HelpModel
=
HelpModel
Help
.
Model
=
Help
.
HelpModel
module
.
exports
=
Help
```
## HTML
Now that you have set up the js side of things we are going to get on to be
setting up the HTML structure. It's important to have HTML because without it
you will not be able to make changes to the back-end of your website with your
admin panel. So without further ado lets get started. The first step you will
need to do is go to your project root and create a new folder and name it view,
in that view folder you will create another folder called admin. This for all
of your HTML files that you will be using to make changes to admin panel. The
first file you will want to create is the login.html and use the following code:
`admin.html`
.
```
html
{{>admin/header}}
<div
class=
"container login-container"
style=
"width: 600px; margin: auto;"
>
<h3><span
class=
"burstload-icon-lg"
></span>
Administrator Login
</h3>
<form
action=
"{{{_uri.login}}}"
method=
"post"
>
<div
class=
"form-group"
>
<label
for=
"email"
>
Email Address
</label>
<input
type=
"text"
name=
"email"
class=
"form-control"
id=
"email"
value=
""
placeholder=
"Email Address"
>
</div>
<div
class=
"form-group"
>
<label
for=
"password"
>
Password
</label>
<input
type=
"password"
name=
"password"
class=
"form-control"
id=
"password"
value=
""
placeholder=
"password"
>
</div>
<button
type=
"submit"
name=
"login"
class=
"btn btn-success container"
><span
class=
"fa fa-lock"
></span>
Login
</button>
</form>
</div>
{{>admin/footer}}
```
The code above will create a back-end login for the admin page so that you can
use it as an administrator and use your email and password.
The next step we will be creating another file within the admin folder and this
one will be called the home.html
`admin.html`
.
```
html
{{>admin/header}}
<div
class=
"container"
>
<h1>
Welcome to the Admin Panel
</h1>
<p>
This is one sweet dashboard!
</p>
</div>
{{>admin/footer}}
```
This is your landing page. This is a blank page where you will get started.
Next we will create a file inside of the admin folder and it will be called your
header.html and in this file we will be using the following code:
`admin.js`
.
```
html
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"utf-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=0"
>
{{#_pageTitle}}
<title>
{{_pageTitle}} - {{_appTitle}}
</title>
{{/_pageTitle}}
{{^_pageTitle}}
<title>
{{_appTitle}}
</title>
{{/_pageTitle}}
</head>
<body>
<nav
class=
"navbar navbar-expand-lg navbar-dark"
>
<a
class=
"navbar-brand"
href=
"/"
><span
class=
"nav-hover"
>
Admin
</span></a>
<button
class=
"navbar-toggler"
type=
"button"
data-toggle=
"collapse"
data-target=
"#navbarSupportedContent"
aria-controls=
"navbarSupportedContent"
aria-expanded=
"false"
aria-label=
"Toggle navigation"
>
<span
class=
"navbar-toggler-icon"
></span>
</button>
<div
class=
"collapse navbar-collapse"
id=
"navbarSupportedContent"
>
<ul
class=
"navbar-nav mr-auto"
>
<li
class=
"nav-item active"
>
<a
class=
"nav-link"
href=
"/admin/"
><span
class=
"nav-hover"
>
Dashboard
</span></a>
</li>
<li
class=
"nav-item active"
>
<a
class=
"nav-link"
href=
"/admin/help/"
><span
class=
"nav-hover"
>
Help
</span></a>
</li>
</ul>
</div>
<span>
</span>
<a
href=
"/admin/logout/"
class=
"btn btn-subscribe"
><span
class=
"fa fa-file-upload"
></span>
Logout
</a>
</nav>
```
The code above will establish the header of your page.
This is the footer.html, the following code is used so that you can create the
bottom of your page.
`admin.js`
.
```
html
{{#_dev}}
<
%&
_pageProfile.HTML
%
>
{{/_dev}}
{{>js}}
</body>
</html>
```
The next file you will be creating is also in your admin folder and will be your
error.html file, this will allow you to set up a place to create for your error
message. Use the following code.
`admin.js`
,
`admin.js`
.
```
html
{{>admin/header}}
<main
role=
"main"
class=
"container"
>
<h1>
An Error has Occurred
</h1>
{{error}}
<br
/><br
/>
</main>
{{>admin/footer}}
```
The last file you will need to add in your admin folder is your js.html file and
this will run a client side JavaScript code on your webpage. the following code
looks like this:
`admin.html`
.
```
html
<script
type=
"text/javascript"
src=
"https://code.jquery.com/jquery-3.3.1.min.js"
></script>
<script
type=
"text/javascript"
src=
"https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
></script>
<script
type=
"text/javascript"
src=
"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
></script>
<script
type=
"text/javascript"
src=
"https://use.fontawesome.com/releases/v5.3.1/js/all.js"
></script>
<script
type=
"text/javascript"
src=
"/js/explode.js"
></script>
<script
type=
"text/javascript"
src=
"/js/FadeIn.js"
></script>
<script
async
defer
src=
"//www.google.com/recaptcha/api.js"
type=
"text/javascript"
></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script
async
src=
"https://www.googletagmanager.com/gtag/js?id=UA-67843687-7"
></script>
<script>
window
.
dataLayer
=
window
.
dataLayer
||
[];
function
gtag
(){
dataLayer
.
push
(
arguments
);}
gtag
(
'
js
'
,
new
Date
());
gtag
(
'
config
'
,
'
UA-67843687-7
'
);
</script>
```
\ No newline at end of file
view/admin/header.html
View file @
6ad46e77
...
...
@@ -2,16 +2,7 @@
<html
lang=
"en"
>
<head>
<meta
charset=
"utf-8"
>
<meta
name=
"keywords"
content=
"cloud hosting, cloud hosting, file storage, file download, file upload, video streaming"
>
<meta
name=
"description"
content=
"Cloud Hosting, Cloud Hosting, File Storage, Video Streaming, File and Folder Sharing, made possible by Burstload"
>
<meta
name=
"author"
content=
"BurstLoad"
>
<meta
property=
"og:site_name"
content=
"BurstLoad"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=0"
>
<link
rel=
"stylesheet"
href=
"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity=
"sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin=
"anonymous"
>
<link
rel=
"stylesheet"
href=
"/css/burstload.css"
>
{{#_css}}
<link
rel=
"stylesheet"
type=
"text/css"
href=
"{{{uri}}}"
>
{{/_css}}
{{#_pageTitle}}
<title>
{{_pageTitle}} - {{_appTitle}}
</title>
{{/_pageTitle}}
...
...
@@ -21,7 +12,7 @@
</head>
<body>
<nav
class=
"navbar navbar-expand-lg navbar-dark"
>
<a
class=
"navbar-brand"
href=
"/"
><span
class=
"burstload-icon-lg"
></span><span
class=
"nav-hover"
>
Admin
</span></a>
<a
class=
"navbar-brand"
href=
"/"
><span
class=
"nav-hover"
>
Admin
</span></a>
<button
class=
"navbar-toggler"
type=
"button"
data-toggle=
"collapse"
data-target=
"#navbarSupportedContent"
aria-controls=
"navbarSupportedContent"
aria-expanded=
"false"
aria-label=
"Toggle navigation"
>
<span
class=
"navbar-toggler-icon"
></span>
</button>
...
...
@@ -30,9 +21,6 @@
<li
class=
"nav-item active"
>
<a
class=
"nav-link"
href=
"/admin/"
><span
class=
"nav-hover"
>
Dashboard
</span></a>
</li>
<li
class=
"nav-item active"
>
<a
class=
"nav-link"
href=
"/admin/user/"
><span
class=
"nav-hover"
>
Users
</span></a>
</li>
<li
class=
"nav-item active"
>
<a
class=
"nav-link"
href=
"/admin/help/"
><span
class=
"nav-hover"
>
Help
</span></a>
</li>
...
...
view/header.html
View file @
6ad46e77
...
...
@@ -2,18 +2,7 @@
<html
lang=
"en"
>
<head>
<meta
charset=
"utf-8"
>
<meta
name=
"keywords"
content=
"cloud hosting, cloud hosting, file storage, file download, file upload, video streaming"
>
<meta
name=
"description"
content=
"Cloud Hosting, Cloud Hosting, File Storage, Video Streaming, File and Folder Sharing, made possible by Burstload"
>
<meta
name=
"author"
content=
"BurstLoad"
>
<meta
property=
"og:site_name"
content=
"BurstLoad"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=0"
>
{{#_bootstrap}}
<link
rel=
"stylesheet"
href=
"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity=
"sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin=
"anonymous"
>
{{/_bootstrap}}
<link
rel=
"stylesheet"
href=
"/css/burstload.css"
>
{{#_css}}
<link
rel=
"stylesheet"
type=
"text/css"
href=
"{{{uri}}}"
>
{{/_css}}
{{#_pageTitle}}
<title>
{{_pageTitle}} - {{_appTitle}}
</title>
{{/_pageTitle}}
...
...
@@ -23,39 +12,21 @@
</head>
<body>
<nav
class=
"navbar navbar-expand-lg navbar-dark"
>
<a
class=
"navbar-brand"
href=
"/"
><span
class=
"burstload-icon-lg"
></span><span
class=
"nav-hover"
>
Admin
</span></a>
<a
class=
"navbar-brand"
href=
"/"
><span
class=
"nav-hover"
>
Admin
</span></a>
<button
class=
"navbar-toggler"
type=
"button"
data-toggle=
"collapse"
data-target=
"#navbarSupportedContent"
aria-controls=
"navbarSupportedContent"
aria-expanded=
"false"
aria-label=
"Toggle navigation"
>
<span
class=
"navbar-toggler-icon"
></span>
</button>
<div
class=
"collapse navbar-collapse"
id=
"navbarSupportedContent"
>
<ul
class=
"navbar-nav mr-auto"
>
<li
class=
"nav-item active"
>
<a
class=
"nav-link"
href=
"/"
><span
class=
"nav-hover"
>
Home
</span></a>
<a
class=
"nav-link"
href=
"/
admin/
"
><span
class=
"nav-hover"
>
Dashboard
</span></a>
</li>
<li
class=
"nav-item active"
>
<a
class=
"nav-link"
href=
"/admin/"
><span
class=
"nav-hover"
>
Admin
</span></a>
</li>
<li
class=
"nav-item active"
>
<a
class=
"nav-link"
href=
"/business-admin/"
><span
class=
"nav-hover"
>
Business Admin
</span></a>
</li>
<li
class=
"nav-item active"
>
<a
class=
"nav-link"
href=
"/features/"
><span
class=
"nav-hover"
>
Features
</span></a>
</li>
<li
class=
"nav-item active"
>
<a
class=
"nav-link"
href=
"/about/"
><span
class=
"nav-hover"
>
About
</span></a>
</li>
<li
class=
"nav-item active"
>
<a
class=
"nav-link"
href=
"/news/"
><span
class=
"nav-hover"
>
News
</span></a>
</li>
<li
class=
"nav-item active"
>
<a
class=
"nav-link"
href=
"/help/"
><span
class=
"nav-hover"
>
Help
</span></a>
</li>
<li
class=
"nav-item active"
>
<a
class=
"nav-link"
href=
"/contact-us/"
><span
class=
"nav-hover"
>
Contact Us
</span></a>
<a
class=
"nav-link"
href=
"/admin/help/"
><span
class=
"nav-hover"
>
Help
</span></a>
</li>
</ul>
</div>
<a
href=
"/login/"
class=
"btn btn-success"
><span
class=
"fa fa-user"
></span>
My Account
</a>
<span>
</span>
<a
href=
"/
order
/"
class=
"btn btn-subscribe"
><span
class=
"fa fa-file-upload"
></span>
Order Now
</a>
<a
href=
"/
admin/logout
/"
class=
"btn btn-subscribe"
><span
class=
"fa fa-file-upload"
></span>
Logout
</a>
</nav>
view/login.html
0 → 100644
View file @
6ad46e77
{{>header}}
<div
class=
"container login-container"
>
<h3>
Login to Burstload
<span
class=
"burstload-icon-lg"
></span></h3>
<form
action=
"{{{_uri.login}}}"
method=
"post"
>
<div
class=
"form-group"
>
<label
for=
"email"
>
Email Address
</label>
<input
type=
"text"
name=
"email"
class=
"form-control"
id=
"email"
value=
""
placeholder=
"Email Address"
>
</div>
<div
class=
"form-group"
>
<label
for=
"password"
>
Password
</label>
<input
type=
"password"
name=
"password"
class=
"form-control"
id=
"password"
value=
""
placeholder=
"password"
>
</div>
<button
type=
"submit"
name=
"login"
class=
"btn btn-success container"
><span
class=
"fa fa-lock"
></span>
Login
</button>
</form>
<div>
Forgot Password?
<a
href=
"{{{_uri.user.forgot_password}}}"
>
Reset Password
</a></div>
<div>
Dont have an account?
<a
href=
"{{{_uri.order}}}"
>
Create One
</a></div>
<div>
Just need help?
<a
href=
"{{{_uri.contact}}}"
>
Contact Us
</a></div>
</div>
{{>footer}}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment