├── .gitignore
├── .Rprofile
├── public
├── img
│ ├── me.jpeg
│ ├── hero-bg.jpg
│ ├── favicons
│ │ ├── favicon.ico
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── apple-touch-icon.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ └── site.webmanifest
│ ├── testimonials
│ │ ├── john.png
│ │ ├── william.jpeg
│ │ ├── zawadi.jpeg
│ │ └── humphreys.jpeg
│ └── skills
│ │ ├── digitalocean.svg
│ │ ├── googlecloud.svg
│ │ ├── git.svg
│ │ ├── r.svg
│ │ ├── javascript.svg
│ │ ├── docker.svg
│ │ └── openapi.svg
├── vendor
│ ├── bootstrap-icons
│ │ └── fonts
│ │ │ ├── bootstrap-icons.woff
│ │ │ └── bootstrap-icons.woff2
│ ├── purecounter
│ │ └── purecounter_vanilla.js
│ ├── imagesloaded
│ │ └── imagesloaded.pkgd.min.js
│ ├── toastr
│ │ ├── toastr-2.1.3.min.js
│ │ └── toastr-2.1.3.min.css
│ ├── bootstrap
│ │ └── css
│ │ │ ├── bootstrap-reboot.min.css
│ │ │ ├── bootstrap-reboot.rtl.min.css
│ │ │ ├── bootstrap-reboot.rtl.css
│ │ │ └── bootstrap-reboot.css
│ ├── glightbox
│ │ └── css
│ │ │ └── glightbox.min.css
│ └── aos
│ │ └── aos.js
├── scss
│ ├── sections
│ │ ├── _starter-section.scss
│ │ ├── _about.scss
│ │ ├── _stats.scss
│ │ ├── _skills.scss
│ │ ├── _resume.scss
│ │ ├── _hero.scss
│ │ ├── _portfolio-details.scss
│ │ ├── _testimonials.scss
│ │ ├── _contact.scss
│ │ ├── _portfolio.scss
│ │ └── _services.scss
│ ├── layouts
│ │ ├── _aos.scss
│ │ ├── _scrolltop.scss
│ │ ├── _page-titles.scss
│ │ ├── _preloader.scss
│ │ ├── _footer.scss
│ │ ├── _header.scss
│ │ ├── _general.scss
│ │ └── _navmenu.scss
│ ├── main.scss
│ ├── _sections.scss
│ └── _variables.scss
└── js
│ ├── toastr_options.js
│ └── main.js
├── controllers
├── resume.pdf
├── mod.R
├── home.R
├── about.R
├── resume.R
├── services.R
└── contact
│ ├── send_message.R
│ ├── handlers.R
│ └── validations.R
├── renv
├── .gitignore
└── settings.json
├── middleware
├── mod.R
├── error.R
└── redirect.R
├── templates
├── path.R
├── page.html
└── partials
│ ├── footer.html
│ └── header.html
├── docker-compose.yml
├── helpers
├── mod.R
├── env_vars.R
└── operators.R
├── Dockerfile
├── Dockerfile.base
├── store
├── page_meta.R
├── create_section_title.R
├── mod.R
├── home.R
├── toastr.R
├── header.R
├── resume.R
├── contact.R
└── about.R
├── generate_dockerfile.R
├── index.R
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | dev.R
2 | .Renviron
3 |
--------------------------------------------------------------------------------
/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/public/img/me.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/img/me.jpeg
--------------------------------------------------------------------------------
/controllers/resume.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/controllers/resume.pdf
--------------------------------------------------------------------------------
/public/img/hero-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/img/hero-bg.jpg
--------------------------------------------------------------------------------
/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/middleware/mod.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . / redirect[redirect],
4 | . / error[error_handler],
5 | )
6 |
--------------------------------------------------------------------------------
/public/img/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/img/favicons/favicon.ico
--------------------------------------------------------------------------------
/public/img/testimonials/john.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/img/testimonials/john.png
--------------------------------------------------------------------------------
/public/img/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/img/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/img/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/img/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/img/testimonials/william.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/img/testimonials/william.jpeg
--------------------------------------------------------------------------------
/public/img/testimonials/zawadi.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/img/testimonials/zawadi.jpeg
--------------------------------------------------------------------------------
/public/img/testimonials/humphreys.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/img/testimonials/humphreys.jpeg
--------------------------------------------------------------------------------
/public/img/favicons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/img/favicons/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/img/favicons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/img/favicons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/img/favicons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/img/favicons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/templates/path.R:
--------------------------------------------------------------------------------
1 | #' Create a path to 'templates/'
2 | #'
3 | #' @export
4 | template_path <- \(...) {
5 | file.path(box::file(), ...)
6 | }
7 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | personal-website:
3 | image: personal-website
4 | ports:
5 | - "1028:8000"
6 | restart: unless-stopped
7 |
--------------------------------------------------------------------------------
/public/vendor/bootstrap-icons/fonts/bootstrap-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/vendor/bootstrap-icons/fonts/bootstrap-icons.woff
--------------------------------------------------------------------------------
/public/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedymwavu/mwavu/HEAD/public/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2
--------------------------------------------------------------------------------
/helpers/mod.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . /
4 | env_vars[
5 | get_env_var,
6 | ],
7 | . /
8 | operators[
9 | `%||%`,
10 | `%!in%`,
11 | ]
12 | )
13 |
--------------------------------------------------------------------------------
/middleware/error.R:
--------------------------------------------------------------------------------
1 | #' Error handler
2 | #'
3 | #' @export
4 | error_handler <- \(req, res, error = NULL) {
5 | if (!is.null(error)) {
6 | msg <- conditionMessage(error)
7 | message(msg)
8 | }
9 |
10 | res$set_status(500L)$send("A server error occurred!")
11 | }
12 |
--------------------------------------------------------------------------------
/public/scss/sections/_starter-section.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Starter Section Section
4 | --------------------------------------------------------------*/
5 | .starter-section {
6 | /* Add your styles here */
7 | }
--------------------------------------------------------------------------------
/public/img/favicons/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/templates/page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [! partials/header.html !]
6 | [% metatags %]
7 | [% title %]
8 |
9 |
10 |
11 | [% content %]
12 |
13 | [! partials/footer.html !]
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/controllers/mod.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . / home[home],
4 | . / about[about],
5 | . /
6 | resume[
7 | resume,
8 | download_resume,
9 | ],
10 | . /
11 | services[
12 | services,
13 | service_detail,
14 | ],
15 | . / contact / handlers[contact_get, contact_post],
16 | )
17 |
--------------------------------------------------------------------------------
/public/scss/layouts/_aos.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Disable aos animation delay on mobile devices
4 | --------------------------------------------------------------*/
5 | @media screen and (max-width: 768px) {
6 | [data-aos-delay] {
7 | transition-delay: 0 !important;
8 | }
9 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM personal-website-base
2 | WORKDIR /app
3 | COPY .Renviron .Renviron
4 | COPY controllers/ controllers/
5 | COPY helpers/ helpers/
6 | COPY index.R index.R
7 | COPY LICENSE LICENSE
8 | COPY middleware/ middleware/
9 | COPY public/ public/
10 | COPY README.md README.md
11 | COPY store/ store/
12 | COPY templates/ templates/
13 | EXPOSE 8000
14 | CMD ["Rscript", "index.R"]
15 |
--------------------------------------------------------------------------------
/Dockerfile.base:
--------------------------------------------------------------------------------
1 | FROM rocker/r-ver:4.5.0
2 | RUN apt-get update && apt-get install -y \
3 | git-core \
4 | libssl-dev \
5 | libxml2-dev \
6 | libcurl4-openssl-dev \
7 | libz-dev
8 | WORKDIR /app
9 | COPY .Rprofile .Rprofile
10 | COPY renv.lock renv.lock
11 | COPY renv/activate.R renv/activate.R
12 | COPY renv/settings.json renv/settings.json
13 | COPY .Renviron .Renviron
14 | RUN R -e "renv::restore()"
15 |
--------------------------------------------------------------------------------
/store/page_meta.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags],
3 | )
4 |
5 | #' Page specific meta tag
6 | #'
7 | #' Added to each page as the "description" meta tag.
8 | #' @param label String. Label eg. "Contact Me"
9 | #' @return [htmltools::tags]
10 | #' @export
11 | page_meta <- \(label = NULL) {
12 | tags$meta(
13 | name = "description",
14 | content = paste("Kennedy Mwavu | Software Developer |", label)
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/public/img/skills/digitalocean.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/js/toastr_options.js:
--------------------------------------------------------------------------------
1 | toastr.options = {
2 | closeButton: true,
3 | debug: false,
4 | newestOnTop: true,
5 | progressBar: true,
6 | positionClass: "toast-bottom-center",
7 | preventDuplicates: false,
8 | onclick: null,
9 | showDuration: "300",
10 | hideDuration: "1000",
11 | timeOut: "0",
12 | extendedTimeOut: "1000",
13 | showEasing: "swing",
14 | hideEasing: "linear",
15 | showMethod: "fadeIn",
16 | hideMethod: "fadeOut",
17 | };
18 |
--------------------------------------------------------------------------------
/store/create_section_title.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags]
3 | )
4 |
5 | #' Create section title
6 | #'
7 | #' @param title String, [htmltools::tags]. The title.
8 | #' @param subtitle String, [htmltools::tags]. Subtitle.
9 | #' @return [htmltools::tags]
10 | #' @export
11 | create_section_title <- \(title = NULL, subtitle = NULL) {
12 | tags$div(
13 | class = "container section-title",
14 | `data-aos` = "fade-up",
15 | tags$h2(title),
16 | tags$p(subtitle)
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/helpers/env_vars.R:
--------------------------------------------------------------------------------
1 | #' Get env var
2 | #'
3 | #' Checks for the env var in the `.Renviron` file.
4 | #' @param name String. Name of the env var to check.
5 | #' @return String. Value of the variable.
6 | #' @export
7 | get_env_var <- function(name) {
8 | key <- Sys.getenv(x = name)
9 | if (identical(key, "")) {
10 | msg <- sprintf(
11 | "Env var `%s` not found! Please set it in your `.Renviron` file.",
12 | name
13 | )
14 | stop(msg, call. = FALSE)
15 | }
16 |
17 | key
18 | }
19 |
--------------------------------------------------------------------------------
/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/controllers/home.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags, tagList],
3 | .. / templates / path[template_path],
4 | .. /
5 | store /
6 | mod[
7 | page_meta,
8 | home_page = home,
9 | ]
10 | )
11 |
12 | #' Handle GET at '/'
13 | #'
14 | #' @export
15 | home <- \(req, res) {
16 | res$render(
17 | template_path("page.html"),
18 | list(
19 | title = "Kennedy Mwavu - Home",
20 | content = home_page(),
21 | metatags = page_meta(label = "Home")
22 | )
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/controllers/about.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags, tagList],
3 | .. /
4 | store /
5 | mod[
6 | page_meta,
7 | about_page = about,
8 | ],
9 | .. / templates / path[template_path],
10 | )
11 |
12 | #' Handle GET at '/about'
13 | #'
14 | #' @export
15 | about <- \(req, res) {
16 | res$render(
17 | template_path("page.html"),
18 | list(
19 | title = "Kennedy Mwavu - About",
20 | content = about_page(),
21 | metatags = page_meta(label = "About")
22 | )
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/store/mod.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . / home[home],
4 | . /
5 | contact[
6 | contact,
7 | text_input,
8 | contact_form,
9 | text_area_input,
10 | default_contact_form,
11 | ],
12 | . /
13 | toastr[
14 | toastr_error,
15 | toastr_success,
16 | ],
17 | . / about[about],
18 | . / resume[resume],
19 | . /
20 | services[
21 | services,
22 | service_detail,
23 | service_detail_title,
24 | service_meta_label,
25 | ],
26 | . / page_meta[page_meta],
27 | )
28 |
--------------------------------------------------------------------------------
/public/scss/main.scss:
--------------------------------------------------------------------------------
1 | // out: ../css/main.css
2 | // You can use the "Easy Compile" VS Code extension to easily compile the scss files https://marketplace.visualstudio.com/items?itemName=refgd.easy-compile
3 |
4 | @import './_variables.scss';
5 |
6 | @import './layouts/_general.scss';
7 | @import './layouts/_header.scss';
8 | @import './layouts/_navmenu.scss';
9 | @import './layouts/_footer.scss';
10 | @import './layouts/_preloader.scss';
11 | @import './layouts/_scrolltop.scss';
12 | @import './layouts/_aos.scss';
13 | @import './layouts/_page-titles.scss';
14 | @import './_sections.scss';
--------------------------------------------------------------------------------
/public/img/skills/googlecloud.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/skills/git.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/scss/sections/_about.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # About Section
4 | --------------------------------------------------------------*/
5 | .about {
6 | .content {
7 | h2 {
8 | font-weight: 700;
9 | font-size: 24px;
10 | }
11 |
12 | ul {
13 | list-style: none;
14 | padding: 0;
15 |
16 | li {
17 | margin-bottom: 20px;
18 | display: flex;
19 | align-items: center;
20 | }
21 |
22 | strong {
23 | margin-right: 10px;
24 | }
25 |
26 | i {
27 | font-size: 16px;
28 | margin-right: 5px;
29 | color: var(--accent-color);
30 | line-height: 0;
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/middleware/redirect.R:
--------------------------------------------------------------------------------
1 | #' Redirect some requests
2 | #'
3 | #' @description
4 | #' I was using GH pages to serve some of my sites + pkgs before. Now, I have
5 | #' to redirect them so that the links are not broken.
6 | #' @details
7 | #' GH pages only accepts GET requests, so I don't have to worry about HTTP
8 | #' methods.
9 | #' @export
10 | redirect <- \(req, res) {
11 | endpoints <- c(
12 | "/blog",
13 | "/rmon",
14 | "/micromodal",
15 | "/ambiorix-examples",
16 | "/frbs",
17 | "/firebase.auth.rest"
18 | )
19 | path <- req$PATH_INFO
20 | cond <- startsWith(x = path, prefix = endpoints) |>
21 | any() |>
22 | isTRUE()
23 |
24 | if (cond) {
25 | to <- paste0("https://kennedymwavu.github.io", path)
26 | return(
27 | res$set_status(301L)$redirect(to)
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/public/scss/layouts/_scrolltop.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Scroll Top Button
4 | --------------------------------------------------------------*/
5 | .scroll-top {
6 | position: fixed;
7 | visibility: hidden;
8 | opacity: 0;
9 | right: 15px;
10 | bottom: -15px;
11 | z-index: 99999;
12 | background-color: var(--accent-color);
13 | width: 44px;
14 | height: 44px;
15 | border-radius: 50px;
16 | transition: all 0.4s;
17 |
18 | i {
19 | font-size: 24px;
20 | color: var(--contrast-color);
21 | line-height: 0;
22 | }
23 |
24 | &:hover {
25 | background-color: color-mix(in srgb, var(--accent-color), transparent 20%);
26 | color: var(--contrast-color);
27 | }
28 |
29 | &.active {
30 | visibility: visible;
31 | opacity: 1;
32 | bottom: 15px;
33 | }
34 | }
--------------------------------------------------------------------------------
/public/img/skills/r.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/skills/javascript.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/generate_dockerfile.R:
--------------------------------------------------------------------------------
1 | all_files <- list.files(path = ".", all.files = TRUE, recursive = TRUE)
2 |
3 | prefixes_to_ignore <- c(
4 | ".git",
5 | "renv",
6 | ".Rprofile",
7 | "dev.R",
8 | "generate_dockerfile.R",
9 | "docker-compose.yml",
10 | "Dockerfile",
11 | "Dockerfile.base"
12 | )
13 | for (prefix in prefixes_to_ignore) {
14 | all_files <- all_files[!startsWith(x = all_files, prefix = prefix)]
15 | }
16 |
17 | all_files <- strsplit(x = all_files, split = "/", fixed = TRUE) |>
18 | sapply(FUN = `[[`, 1L) |>
19 | unique()
20 |
21 | is_dir <- dir.exists(all_files)
22 | all_files[is_dir] <- paste0(all_files[is_dir], "/")
23 |
24 | copy_statements <- sprintf("COPY %s %s", all_files, all_files) |>
25 | paste(collapse = "\n")
26 |
27 | dockerfile <- paste(
28 | "FROM personal-website-base",
29 | "WORKDIR /app",
30 | copy_statements,
31 | "EXPOSE 8000",
32 | 'CMD ["Rscript", "index.R"]',
33 | sep = "\n"
34 | )
35 |
36 | cat(
37 | dockerfile,
38 | "\n",
39 | file = "Dockerfile",
40 | append = FALSE
41 | )
42 |
--------------------------------------------------------------------------------
/controllers/resume.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags, tagList],
3 | .. / templates / path[template_path],
4 | .. /
5 | store /
6 | mod[
7 | page_meta,
8 | resume_page = resume,
9 | ],
10 | )
11 |
12 | #' Handle GET at '/resume'
13 | #'
14 | #' @export
15 | resume <- \(req, res) {
16 | res$render(
17 | template_path("page.html"),
18 | list(
19 | title = "Kennedy Mwavu - Resume",
20 | content = resume_page(),
21 | metatags = page_meta(label = "Resume")
22 | )
23 | )
24 | }
25 |
26 | #' Handle GET at '/download-resume'
27 | #'
28 | #' @export
29 | download_resume <- \(req, res) {
30 | path <- file.path(
31 | box::file(),
32 | "resume.pdf"
33 | )
34 |
35 | binary <- readBin(
36 | con = path,
37 | what = "raw",
38 | n = file.info(path)$size
39 | )
40 |
41 | res$header("Content-Type", "application/pdf")
42 | res$header(
43 | "Content-Disposition",
44 | "attachment; filename=KennedyMwavu-SoftwareDeveloper-Resume.pdf"
45 | )
46 | res$send(binary)
47 | }
48 |
--------------------------------------------------------------------------------
/public/scss/layouts/_page-titles.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Global Page Titles & Breadcrumbs
4 | --------------------------------------------------------------*/
5 | .page-title {
6 | color: var(--default-color);
7 | background-color: var(--background-color);
8 | padding: 25px 0;
9 | position: relative;
10 | border-bottom: 1px solid color-mix(in srgb, var(--default-color), transparent 85%);
11 |
12 | h1 {
13 | font-size: 24px;
14 | font-weight: 400;
15 | }
16 |
17 | .breadcrumbs {
18 | ol {
19 | display: flex;
20 | flex-wrap: wrap;
21 | list-style: none;
22 | padding: 0;
23 | margin: 0;
24 | font-size: 14px;
25 | font-weight: 400;
26 |
27 | li+li {
28 | padding-left: 10px;
29 | }
30 |
31 | li+li::before {
32 | content: "/";
33 | display: inline-block;
34 | padding-right: 10px;
35 | color: color-mix(in srgb, var(--default-color), transparent 70%);
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/public/scss/sections/_stats.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Stats Section
4 | --------------------------------------------------------------*/
5 | .stats {
6 | .stats-item {
7 | padding: 30px;
8 | width: 100%;
9 |
10 | span {
11 | color: var(--heading-color);
12 | font-size: 48px;
13 | display: block;
14 | font-weight: 700;
15 | margin-bottom: 20px;
16 | padding-bottom: 20px;
17 | position: relative;
18 |
19 | &:after {
20 | content: '';
21 | position: absolute;
22 | display: block;
23 | width: 25px;
24 | height: 3px;
25 | background: var(--accent-color);
26 | left: 0;
27 | right: 0;
28 | bottom: 0;
29 | margin: auto;
30 | }
31 | }
32 |
33 | p {
34 | color: color-mix(in srgb, var(--default-color), transparent 40%);
35 | padding: 0;
36 | margin: 0;
37 | font-family: var(--heading-font);
38 | font-weight: 500;
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/public/scss/layouts/_preloader.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Preloader
4 | --------------------------------------------------------------*/
5 | #preloader {
6 | position: fixed;
7 | inset: 0;
8 | z-index: 9999;
9 | overflow: hidden;
10 | background-color: var(--background-color);
11 | transition: all 0.6s ease-out;
12 | width: 100%;
13 | height: 100vh;
14 |
15 | &:before,
16 | &:after {
17 | content: "";
18 | position: absolute;
19 | border: 4px solid var(--accent-color);
20 | border-radius: 50%;
21 | animation: animate-preloader 2s cubic-bezier(0, 0.2, 0.8, 1) infinite;
22 | }
23 |
24 | &:after {
25 | animation-delay: -0.5s;
26 | }
27 | }
28 |
29 | @keyframes animate-preloader {
30 | 0% {
31 | width: 10px;
32 | height: 10px;
33 | top: calc(50% - 5px);
34 | left: calc(50% - 5px);
35 | opacity: 1;
36 | }
37 |
38 | 100% {
39 | width: 72px;
40 | height: 72px;
41 | top: calc(50% - 36px);
42 | left: calc(50% - 36px);
43 | opacity: 0;
44 | }
45 | }
--------------------------------------------------------------------------------
/store/home.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags, tagList],
3 | . / header[header],
4 | )
5 |
6 | #' Home page
7 | #'
8 | #' @export
9 | home <- \() {
10 | tagList(
11 | header(active = "Home"),
12 | tags$main(
13 | class = "main",
14 | hero()
15 | )
16 | )
17 | }
18 |
19 | #' Hero section
20 | #'
21 | #' @export
22 | hero <- \() {
23 | tags$section(
24 | id = "hero",
25 | class = "hero section",
26 | tags$img(
27 | src = "assets/img/hero-bg.jpg",
28 | alt = "",
29 | `data-aos` = "fade-in"
30 | ),
31 | tags$div(
32 | class = "container text-center",
33 | `data-aos` = "zoom-out",
34 | `data-aos-delay` = "100",
35 | tags$div(
36 | class = "row justify-content-center",
37 | tags$div(
38 | class = "col-lg-8",
39 | tags$h2("Kennedy Mwavu"),
40 | tags$p("From code to canvas, I paint your R vision"),
41 | tags$a(
42 | href = "/about",
43 | class = "btn-get-started",
44 | "About Me"
45 | )
46 | )
47 | )
48 | )
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/helpers/operators.R:
--------------------------------------------------------------------------------
1 | #' Coalescing operator to specify a default value
2 | #'
3 | #' This operator is used to specify a default value for a variable if the
4 | #' original value is \code{NULL}.
5 | #'
6 | #' @param x a variable to check for \code{NULL}
7 | #' @param y the default value to return if \code{x} is \code{NULL}
8 | #' @return the first non-\code{NULL} value
9 | #' @name op-null-default
10 | #' @examples
11 | #' my_var <- NULL
12 | #' default_value <- "hello"
13 | #' result <- my_var %||% default_value
14 | #' result # "hello"
15 | #'
16 | #' my_var <- "world"
17 | #' default_value <- "hello"
18 | #' result <- my_var %||% default_value
19 | #' result # "world"
20 | #'
21 | #' @export
22 | "%||%" <- \(x, y) {
23 | if (is.null(x)) y else x
24 | }
25 |
26 | #' Not-in operator
27 | #'
28 | #' This operator of the opposite of the `%in%` operator
29 | #' @param x The vector to be checked for.
30 | #' @param y The vector to check in.
31 | #' @examples
32 | #' "a" %!in% letters # FALSE, 'a' is in letters
33 | #' "aa" %!in% letters # TRUE, 'aa' is NOT in letters
34 | #' @return Logical.
35 | #' @export
36 | "%!in%" <- Negate(`%in%`)
37 |
--------------------------------------------------------------------------------
/controllers/services.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | .. / templates / path[template_path],
3 | .. /
4 | store /
5 | mod[
6 | page_meta,
7 | services_page = services,
8 | service_detail_page = service_detail,
9 | service_detail_title,
10 | service_meta_label,
11 | ],
12 | )
13 |
14 | #' Handle GET at '/services'
15 | #'
16 | #' @export
17 | services <- \(req, res) {
18 | res$render(
19 | template_path("page.html"),
20 | list(
21 | title = "Kennedy Mwavu - Services",
22 | content = services_page(),
23 | metatags = page_meta(label = "Services")
24 | )
25 | )
26 | }
27 |
28 | #' Build handler for service detail routes
29 | #'
30 | #' @param slug String. Service identifier.
31 | #' @return function
32 | #' @export
33 | service_detail <- \(slug) {
34 | \(req, res) {
35 | title <- service_detail_title(slug)
36 |
37 | res$render(
38 | template_path("page.html"),
39 | list(
40 | title = sprintf("Kennedy Mwavu - %s", title),
41 | content = service_detail_page(slug),
42 | metatags = page_meta(label = service_meta_label(slug))
43 | )
44 | )
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/public/scss/layouts/_footer.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Global Footer
4 | --------------------------------------------------------------*/
5 | .footer {
6 | color: var(--default-color);
7 | background-color: var(--background-color);
8 | font-size: 14px;
9 | padding: 40px 0;
10 | position: relative;
11 |
12 | .copyright {
13 | p {
14 | margin-bottom: 0;
15 | }
16 | }
17 |
18 | .social-links {
19 | margin-top: 20px;
20 |
21 | a {
22 | display: flex;
23 | align-items: center;
24 | justify-content: center;
25 | width: 40px;
26 | height: 40px;
27 | border-radius: 50%;
28 | border: 1px solid color-mix(in srgb, var(--default-color), transparent 50%);
29 | font-size: 16px;
30 | color: color-mix(in srgb, var(--default-color), transparent 50%);
31 | margin: 0 5px;
32 | transition: 0.3s;
33 |
34 | &:hover {
35 | color: var(--accent-color);
36 | border-color: var(--accent-color);
37 | }
38 | }
39 | }
40 |
41 | .credits {
42 | margin-top: 10px;
43 | font-size: 13px;
44 | text-align: center;
45 | }
46 | }
--------------------------------------------------------------------------------
/public/scss/sections/_skills.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Skills Section
4 | --------------------------------------------------------------*/
5 | .skills {
6 | .skills-grid {
7 | margin: 0;
8 |
9 | .skill-card {
10 | background: var(--surface-color);
11 | border: 1px solid color-mix(in srgb, var(--default-color) 12%, transparent);
12 | border-radius: 16px;
13 | padding: 1rem 1.25rem;
14 | min-height: 86px;
15 | transition: transform 0.2s ease, box-shadow 0.2s ease;
16 |
17 | &:hover {
18 | transform: translateY(-2px);
19 | box-shadow: 0 1rem 2rem -1rem color-mix(in srgb, var(--default-color) 60%, transparent);
20 | }
21 |
22 | &__logo {
23 | width: 48px;
24 | height: 48px;
25 | border-radius: 12px;
26 | background: color-mix(in srgb, var(--accent-color) 15%, transparent);
27 | padding: 10px;
28 |
29 | img {
30 | width: 100%;
31 | height: 100%;
32 | object-fit: contain;
33 | }
34 | }
35 |
36 | &__label {
37 | color: var(--heading-color);
38 | letter-spacing: 0.08em;
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/public/scss/layouts/_header.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Global Header
4 | --------------------------------------------------------------*/
5 | .header {
6 | color: var(--default-color);
7 | background-color: var(--background-color);
8 | padding: 15px 0;
9 | transition: all 0.5s;
10 | z-index: 997;
11 | box-shadow: 0px 0 18px rgba(0, 0, 0, 0.1);
12 |
13 | .logo {
14 | line-height: 1;
15 |
16 | img {
17 | max-height: 36px;
18 | margin-right: 8px;
19 | }
20 |
21 | h1 {
22 | font-size: 32px;
23 | margin: 0;
24 | font-weight: 300;
25 | text-transform: uppercase;
26 | color: var(--heading-color);
27 | }
28 | }
29 |
30 | .header-social-links {
31 | padding-right: 15px;
32 |
33 | a {
34 | color: color-mix(in srgb, var(--default-color), transparent 40%);
35 | padding-left: 6px;
36 | display: inline-block;
37 | transition: 0.3s;
38 | font-size: 16px;
39 |
40 | &:hover {
41 | color: var(--accent-color);
42 | }
43 |
44 | i {
45 | line-height: 0px;
46 | }
47 | }
48 | }
49 |
50 | @media (max-width: 1200px) {
51 | .logo {
52 | order: 1;
53 | }
54 |
55 | .header-social-links {
56 | order: 2;
57 | }
58 |
59 | .navmenu {
60 | order: 3;
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/index.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Ambiorix],
3 | . /
4 | controllers /
5 | mod[
6 | home,
7 | about,
8 | resume,
9 | services,
10 | service_detail,
11 | contact_get,
12 | contact_post,
13 | download_resume,
14 | ],
15 | . /
16 | middleware /
17 | mod[
18 | redirect,
19 | error_handler,
20 | ],
21 | )
22 |
23 | Ambiorix$new(host = "0.0.0.0", port = 8000)$set_error(error_handler)$use(
24 | redirect
25 | )$static("public", "assets")$get("/", home)$get("/about", about)$get(
26 | "/resume",
27 | resume
28 | )$get("/services", services)$get(
29 | "/services/custom-software-development",
30 | service_detail("custom-software-development")
31 | )$get(
32 | "/services/web-application-development",
33 | service_detail("web-application-development")
34 | )$get(
35 | "/services/api-integration-and-development",
36 | service_detail("api-integration-and-development")
37 | )$get(
38 | "/services/software-maintenance-and-support",
39 | service_detail("software-maintenance-and-support")
40 | )$get(
41 | "/services/consulting-and-advisory",
42 | service_detail("consulting-and-advisory")
43 | )$get(
44 | "/services/technical-training-and-workshops",
45 | service_detail("technical-training-and-workshops")
46 | )$get("/contact", contact_get)$post("/contact", contact_post)$get(
47 | "/download-resume",
48 | download_resume
49 | )$start()
50 |
--------------------------------------------------------------------------------
/public/scss/sections/_resume.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Resume Section
4 | --------------------------------------------------------------*/
5 | .resume {
6 | .resume-title {
7 | color: var(--heading-color);
8 | font-size: 26px;
9 | font-weight: 700;
10 | margin-top: 20px;
11 | margin-bottom: 20px;
12 | }
13 |
14 | .resume-item {
15 | padding: 0 0 20px 20px;
16 | margin-top: -2px;
17 | border-left: 2px solid var(--accent-color);
18 | position: relative;
19 |
20 | h4 {
21 | line-height: 18px;
22 | font-size: 18px;
23 | font-weight: 600;
24 | text-transform: uppercase;
25 | color: color-mix(in srgb, var(--default-color), transparent 20%);
26 | margin-bottom: 10px;
27 | }
28 |
29 | h5 {
30 | font-size: 16px;
31 | padding: 5px 15px;
32 | display: inline-block;
33 | font-weight: 600;
34 | margin-bottom: 10px;
35 | }
36 |
37 | ul {
38 | padding-left: 20px;
39 |
40 | li {
41 | padding-bottom: 10px;
42 | }
43 | }
44 |
45 | &:last-child {
46 | padding-bottom: 0;
47 | }
48 | }
49 |
50 | .resume-item::before {
51 | content: "";
52 | position: absolute;
53 | width: 16px;
54 | height: 16px;
55 | border-radius: 50px;
56 | left: -9px;
57 | top: 0;
58 | background: var(--background-color);
59 | border: 2px solid var(--accent-color);
60 | }
61 | }
--------------------------------------------------------------------------------
/public/scss/sections/_hero.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Hero Section
4 | --------------------------------------------------------------*/
5 | .hero {
6 | width: 100%;
7 | min-height: calc(100vh - 82px);
8 | position: relative;
9 | padding: 80px 0;
10 | display: flex;
11 | align-items: center;
12 | justify-content: center;
13 |
14 | @media (max-width: 1200px) {
15 | min-height: calc(100vh - 68px);
16 | }
17 |
18 | img {
19 | position: absolute;
20 | inset: 0;
21 | display: block;
22 | width: 100%;
23 | height: 100%;
24 | object-fit: cover;
25 | z-index: 1;
26 | }
27 |
28 | .container {
29 | position: relative;
30 | z-index: 3;
31 | }
32 |
33 | h2 {
34 | margin: 0;
35 | font-size: 48px;
36 | font-weight: 700;
37 | }
38 |
39 | p {
40 | margin: 10px 0 0 0;
41 | font-size: 24px;
42 | color: var(--heading-color);
43 | }
44 |
45 | .btn-get-started {
46 | color: var(--contrast-color);
47 | background: var(--accent-color);
48 | font-family: var(--heading-font);
49 | text-transform: uppercase;
50 | font-weight: 600;
51 | font-size: 12px;
52 | letter-spacing: 1px;
53 | display: inline-block;
54 | padding: 12px 40px;
55 | border-radius: 50px;
56 | transition: 0.5s;
57 | margin-top: 30px;
58 |
59 | &:hover {
60 | background: color-mix(in srgb, var(--accent-color) 90%, white 20%);
61 | }
62 | }
63 |
64 | @media (max-width: 768px) {
65 | h2 {
66 | font-size: 32px;
67 | }
68 |
69 | p {
70 | font-size: 18px;
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/public/scss/sections/_portfolio-details.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Portfolio Details Section
4 | --------------------------------------------------------------*/
5 | .portfolio-details {
6 | .portfolio-details-slider {
7 | img {
8 | width: 100%;
9 | }
10 |
11 | .swiper-pagination {
12 | margin-top: 20px;
13 | position: relative;
14 |
15 | .swiper-pagination-bullet {
16 | width: 12px;
17 | height: 12px;
18 | background-color: color-mix(in srgb, var(--default-color), transparent 85%);
19 | opacity: 1;
20 | }
21 |
22 | .swiper-pagination-bullet-active {
23 | background-color: var(--accent-color);
24 | }
25 | }
26 | }
27 |
28 | .portfolio-info {
29 | background-color: var(--surface-color);
30 | padding: 30px;
31 | box-shadow: 0px 0 30px rgba(0, 0, 0, 0.1);
32 |
33 | h3 {
34 | font-size: 22px;
35 | font-weight: 700;
36 | margin-bottom: 20px;
37 | padding-bottom: 20px;
38 | border-bottom: 1px solid color-mix(in srgb, var(--default-color), transparent 85%);
39 | }
40 |
41 | ul {
42 | list-style: none;
43 | padding: 0;
44 | font-size: 15px;
45 |
46 | li+li {
47 | margin-top: 10px;
48 | }
49 | }
50 | }
51 |
52 | .portfolio-description {
53 | padding-top: 30px;
54 |
55 | h2 {
56 | font-size: 26px;
57 | font-weight: 700;
58 | margin-bottom: 20px;
59 | }
60 |
61 | p {
62 | padding: 0;
63 | color: color-mix(in srgb, var(--default-color), transparent 30%);
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/store/toastr.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags],
3 | .. / helpers / mod[`%||%`]
4 | )
5 |
6 | #' Toastr
7 | #'
8 | #' @param type String. Type of the toast. Valid options are:
9 | #' - info
10 | #' - warning
11 | #' - success
12 | #' - error
13 | #' @param title String. Title of the toast. Defaults to `NULL`.
14 | #' @param msg String. Message to show in the toast. Defaults to `NULL`.
15 | #' @return An object of class `shiny.tag`.
16 | #' @export
17 | toastr <- \(
18 | type = c("info", "warning", "success", "error"),
19 | title = NULL,
20 | msg = NULL
21 | ) {
22 | type <- match.arg(arg = type)
23 | title <- title %||% ""
24 | msg <- msg %||% ""
25 |
26 | tags$script(
27 | sprintf(
28 | 'toastr["%s"]("%s", "%s");',
29 | type,
30 | msg,
31 | title
32 | )
33 | )
34 | }
35 |
36 | #' Info toastr
37 | #'
38 | #' @inheritParams toastr
39 | #' @inherit toastr return
40 | #' @export
41 | toastr_info <- \(title = "", msg = NULL) {
42 | toastr(type = "info", title = title, msg = msg)
43 | }
44 |
45 | #' Warning toastr
46 | #'
47 | #' @inheritParams toastr
48 | #' @inherit toastr return
49 | #' @export
50 | toastr_warning <- \(title = "Warning!", msg = NULL) {
51 | toastr(type = "warning", title = title, msg = msg)
52 | }
53 |
54 | #' Success toastr
55 | #'
56 | #' @inheritParams toastr
57 | #' @inherit toastr return
58 | #' @export
59 | toastr_success <- \(title = "", msg = NULL) {
60 | toastr(type = "success", title = title, msg = msg)
61 | }
62 |
63 | #' Error toastr
64 | #'
65 | #' @inheritParams toastr
66 | #' @inherit toastr return
67 | #' @export
68 | toastr_error <- \(title = "Error!", msg = NULL) {
69 | toastr(type = "error", title = title, msg = msg)
70 | }
71 |
--------------------------------------------------------------------------------
/public/img/skills/docker.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/scss/_sections.scss:
--------------------------------------------------------------------------------
1 | /*--------------------------------------------------------------
2 | # Global Sections
3 | --------------------------------------------------------------*/
4 | section,
5 | .section {
6 | color: var(--default-color);
7 | background-color: var(--background-color);
8 | padding: 60px 0;
9 | scroll-margin-top: 100px;
10 | overflow: clip;
11 | }
12 |
13 | @media (max-width: 1199px) {
14 |
15 | section,
16 | .section {
17 | scroll-margin-top: 66px;
18 | }
19 | }
20 |
21 | /*--------------------------------------------------------------
22 | # Global Section Titles
23 | --------------------------------------------------------------*/
24 | .section-title {
25 | text-align: center;
26 | padding-bottom: 60px;
27 | position: relative;
28 |
29 | h2 {
30 | font-size: 32px;
31 | font-weight: 700;
32 | margin-bottom: 20px;
33 | padding-bottom: 20px;
34 | position: relative;
35 |
36 | &:after {
37 | content: '';
38 | position: absolute;
39 | display: block;
40 | width: 50px;
41 | height: 3px;
42 | background: var(--accent-color);
43 | left: 0;
44 | right: 0;
45 | bottom: 0;
46 | margin: auto;
47 | }
48 | }
49 |
50 | p {
51 | margin-bottom: 0;
52 | }
53 | }
54 |
55 | @import './sections/_hero.scss';
56 | @import './sections/_about.scss';
57 | @import './sections/_skills.scss';
58 | @import './sections/_stats.scss';
59 | @import './sections/_testimonials.scss';
60 | @import './sections/_resume.scss';
61 | @import './sections/_services.scss';
62 | @import './sections/_portfolio.scss';
63 | @import './sections/_portfolio-details.scss';
64 | @import './sections/_contact.scss';
65 | @import './sections/_starter-section.scss';
--------------------------------------------------------------------------------
/controllers/contact/send_message.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | httr2[
3 | request,
4 | req_headers,
5 | req_perform,
6 | req_body_json,
7 | resp_body_json,
8 | ],
9 | .. /
10 | .. /
11 | helpers /
12 | mod[
13 | get_env_var,
14 | ]
15 | )
16 |
17 | #' Send message via email
18 | #'
19 | #' @details This forwards the entered message to my inbox
20 | #' @param client_name String. Name of sender.
21 | #' @param client_email String. Email of sender.
22 | #' @param subject String. Email subject.
23 | #' @param message String. Message.
24 | #' @return Named list, the most important being:
25 | #' - `ok`: Logical. Was message forwarded to my inbox successfully?
26 | #' @export
27 | send_message <- function(
28 | client_name,
29 | client_email,
30 | subject,
31 | message
32 | ) {
33 | subject <- sprintf("%s (from %s)", subject, client_name)
34 |
35 | req <- request(base_url = "https://api.postmarkapp.com/email") |>
36 | req_headers(
37 | Accept = "application/json",
38 | `Content-Type` = "application/json",
39 | `X-Postmark-Server-Token` = get_env_var(name = "POSTMARK_SERVER_TOKEN"),
40 | ) |>
41 | req_body_json(
42 | data = list(
43 | From = get_env_var(name = "EMAIL"),
44 | To = get_env_var(name = "EMAIL"),
45 | ReplyTo = client_email,
46 | Subject = subject,
47 | TextBody = message,
48 | MessageStream = "outbound"
49 | )
50 | )
51 |
52 | tryCatch(
53 | expr = {
54 | out <- req |>
55 | req_perform() |>
56 | resp_body_json()
57 |
58 | out$ok <- identical(out$ErrorCode, 0L)
59 | out
60 | },
61 | error = \(e) {
62 | print(conditionMessage(e))
63 | list(ok = FALSE)
64 | }
65 | )
66 | }
67 |
--------------------------------------------------------------------------------
/public/scss/layouts/_general.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # General Styling & Shared Classes
4 | --------------------------------------------------------------*/
5 | body {
6 | color: var(--default-color);
7 | background-color: var(--background-color);
8 | font-family: var(--default-font);
9 | }
10 |
11 | a {
12 | color: var(--accent-color);
13 | text-decoration: none;
14 | transition: 0.3s;
15 | }
16 |
17 | a:hover {
18 | color: color-mix(in srgb, var(--accent-color), transparent 25%);
19 | text-decoration: none;
20 | }
21 |
22 | h1,
23 | h2,
24 | h3,
25 | h4,
26 | h5,
27 | h6 {
28 | color: var(--heading-color);
29 | font-family: var(--heading-font);
30 | }
31 |
32 | /* PHP Email Form Messages
33 | ------------------------------*/
34 | .php-email-form {
35 | .error-message {
36 | display: none;
37 | background: #df1529;
38 | color: #ffffff;
39 | text-align: left;
40 | padding: 15px;
41 | margin-bottom: 24px;
42 | font-weight: 600;
43 | }
44 |
45 | .sent-message {
46 | display: none;
47 | color: #ffffff;
48 | background: #059652;
49 | text-align: center;
50 | padding: 15px;
51 | margin-bottom: 24px;
52 | font-weight: 600;
53 | }
54 |
55 | .loading {
56 | display: none;
57 | background: var(--surface-color);
58 | text-align: center;
59 | padding: 15px;
60 | margin-bottom: 24px;
61 |
62 | &:before {
63 | content: "";
64 | display: inline-block;
65 | border-radius: 50%;
66 | width: 24px;
67 | height: 24px;
68 | margin: 0 10px -6px 0;
69 | border: 3px solid var(--accent-color);
70 | border-top-color: var(--surface-color);
71 | animation: php-email-form-loading 1s linear infinite;
72 | }
73 | }
74 | }
75 |
76 | @keyframes php-email-form-loading {
77 | 0% {
78 | transform: rotate(0deg);
79 | }
80 |
81 | 100% {
82 | transform: rotate(360deg);
83 | }
84 | }
--------------------------------------------------------------------------------
/templates/partials/footer.html:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/public/scss/sections/_testimonials.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Testimonials Section
4 | --------------------------------------------------------------*/
5 | .testimonials {
6 | .section-header {
7 | margin-bottom: 40px;
8 | }
9 |
10 | .testimonials-carousel,
11 | .testimonials-slider {
12 | overflow: hidden;
13 | }
14 |
15 | .testimonial-item {
16 | text-align: center;
17 |
18 | .testimonial-img {
19 | width: 120px;
20 | border-radius: 50%;
21 | border: 4px solid var(--background-color);
22 | margin: 0 auto;
23 | }
24 |
25 | h3 {
26 | font-size: 20px;
27 | font-weight: bold;
28 | margin: 10px 0 5px 0;
29 | }
30 |
31 | h4 {
32 | font-size: 14px;
33 | color: color-mix(in srgb, var(--default-color), transparent 40%);
34 | margin: 0 0 15px 0;
35 | }
36 |
37 | .stars {
38 | margin-bottom: 15px;
39 |
40 | i {
41 | color: #ffc107;
42 | margin: 0 1px;
43 | }
44 | }
45 |
46 | .quote-icon-left,
47 | .quote-icon-right {
48 | color: color-mix(in srgb, var(--accent-color), transparent 50%);
49 | font-size: 26px;
50 | line-height: 0;
51 | }
52 |
53 | .quote-icon-left {
54 | display: inline-block;
55 | left: -5px;
56 | position: relative;
57 | }
58 |
59 | .quote-icon-right {
60 | display: inline-block;
61 | right: -5px;
62 | position: relative;
63 | top: 10px;
64 | transform: scale(-1, -1);
65 | }
66 |
67 | p {
68 | font-style: italic;
69 | margin: 0 auto 15px auto;
70 | }
71 | }
72 |
73 | .swiper-wrapper {
74 | height: auto;
75 | }
76 |
77 | .swiper-pagination {
78 | margin-top: 20px;
79 | position: relative;
80 |
81 | .swiper-pagination-bullet {
82 | width: 12px;
83 | height: 12px;
84 | opacity: 1;
85 | background-color: color-mix(in srgb, var(--default-color), transparent 85%);
86 | }
87 |
88 | .swiper-pagination-bullet-active {
89 | background-color: var(--accent-color);
90 | }
91 | }
92 |
93 | @media (min-width: 992px) {
94 | .testimonial-item p {
95 | width: 80%;
96 | }
97 | }
98 | }
--------------------------------------------------------------------------------
/templates/partials/header.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/scss/sections/_contact.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Contact Section
4 | --------------------------------------------------------------*/
5 | .contact {
6 | .info-wrap {
7 | background-color: var(--surface-color);
8 | box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.1);
9 | padding: 30px;
10 |
11 | @media (max-width: 575px) {
12 | padding: 20px;
13 | }
14 | }
15 |
16 | .info-item {
17 | margin-bottom: 40px;
18 |
19 | i {
20 | font-size: 20px;
21 | color: var(--accent-color);
22 | background: color-mix(in srgb, var(--accent-color), transparent 92%);
23 | width: 44px;
24 | height: 44px;
25 | display: flex;
26 | justify-content: center;
27 | align-items: center;
28 | border-radius: 50px;
29 | transition: all 0.3s ease-in-out;
30 | margin-right: 15px;
31 | }
32 |
33 | h3 {
34 | padding: 0;
35 | font-size: 18px;
36 | font-weight: 700;
37 | margin-bottom: 5px;
38 | }
39 |
40 | p {
41 | padding: 0;
42 | margin-bottom: 0;
43 | font-size: 14px;
44 | }
45 |
46 | &:hover {
47 | i {
48 | background: var(--accent-color);
49 | color: var(--contrast-color);
50 | }
51 | }
52 | }
53 |
54 | .php-email-form {
55 | background-color: var(--surface-color);
56 | height: 100%;
57 | padding: 30px;
58 | box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.1);
59 |
60 | @media (max-width: 575px) {
61 | padding: 20px;
62 | }
63 |
64 | input[type=text],
65 | input[type=email],
66 | textarea {
67 | font-size: 14px;
68 | padding: 10px 15px;
69 | box-shadow: none;
70 | border-radius: 0;
71 | color: var(--default-color);
72 | background-color: var(--surface-color);
73 | border-color: color-mix(in srgb, var(--default-color), transparent 80%);
74 |
75 | &:focus {
76 | border-color: var(--accent-color);
77 | }
78 |
79 | &::placeholder {
80 | color: color-mix(in srgb, var(--default-color), transparent 70%);
81 | }
82 | }
83 |
84 | button[type="submit"] {
85 | color: var(--contrast-color);
86 | background: var(--accent-color);
87 | border: 0;
88 | padding: 10px 30px;
89 | transition: 0.4s;
90 | border-radius: 50px;
91 |
92 | &:hover {
93 | background: color-mix(in srgb, var(--accent-color), transparent 25%);
94 | }
95 | }
96 | }
97 | }
--------------------------------------------------------------------------------
/controllers/contact/handlers.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | mime, # used by `ambiorix::parse_multipart()`
3 | htmltools[tags, tagList],
4 | ambiorix[parse_multipart],
5 | . / send_message[send_message],
6 | . /
7 | validations[
8 | validate_name,
9 | validate_email,
10 | validate_message,
11 | validate_subject,
12 | ],
13 | .. /
14 | .. /
15 | store /
16 | mod[
17 | page_meta,
18 | contact_page = contact,
19 | contact_form,
20 | toastr_error,
21 | toastr_success,
22 | default_contact_form,
23 | ],
24 | .. / .. / templates / path[template_path],
25 | )
26 |
27 | #' Handle GET at '/contact'
28 | #'
29 | #' @export
30 | contact_get <- \(req, res) {
31 | res$render(
32 | template_path("page.html"),
33 | list(
34 | title = "Kennedy Mwavu - Contact",
35 | content = contact_page(),
36 | metatags = page_meta(label = "Contact Me")
37 | )
38 | )
39 | }
40 |
41 |
42 | #' Handle POST at '/contact'
43 | #'
44 | #' @export
45 | contact_post <- \(req, res) {
46 | body <- parse_multipart(req)
47 | name <- validate_name(body$name)
48 | email <- validate_email(body$email)
49 | subject <- validate_subject(body$subject)
50 | message <- validate_message(body$message)
51 |
52 | validated_form <- contact_form(
53 | name = name$validations,
54 | email = email$validations,
55 | subject = subject$validations,
56 | message = message$validations
57 | )
58 | container <- \(...) {
59 | tags$div(
60 | id = "form_container",
61 | class = "col-lg-7",
62 | ...
63 | )
64 | }
65 |
66 | all_ok <- name$ok && email$ok && subject$ok && message$ok
67 | if (!all_ok) {
68 | return(
69 | res$send(
70 | container(validated_form)
71 | )
72 | )
73 | }
74 |
75 | sent <- send_message(
76 | client_name = name$value,
77 | client_email = email$value,
78 | subject = subject$value,
79 | message = message$value
80 | )
81 |
82 | if (!sent$ok) {
83 | response <- container(
84 | toastr_error(
85 | msg = "An error occurred while sending your message. Please retry."
86 | ),
87 | validated_form
88 | )
89 | return(
90 | res$send(response)
91 | )
92 | }
93 |
94 | msg <- sprintf(
95 | "Thank you, %s! Your message has been sent. I'll get back to you soon.",
96 | name$value
97 | )
98 | response <- container(
99 | toastr_success(msg = msg),
100 | default_contact_form()
101 | )
102 |
103 | res$send(response)
104 | }
105 |
--------------------------------------------------------------------------------
/public/scss/sections/_portfolio.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Portfolio Section
4 | --------------------------------------------------------------*/
5 | .portfolio {
6 | .portfolio-filters {
7 | padding: 0;
8 | margin: 0 auto 20px auto;
9 | list-style: none;
10 | text-align: center;
11 |
12 | li {
13 | cursor: pointer;
14 | display: inline-block;
15 | padding: 8px 20px 10px 20px;
16 | margin: 0;
17 | font-size: 15px;
18 | font-weight: 500;
19 | line-height: 1;
20 | margin-bottom: 5px;
21 | border-radius: 50px;
22 | transition: all 0.3s ease-in-out;
23 | font-family: var(--heading-font);
24 |
25 | &:hover,
26 | &.filter-active {
27 | color: var(--contrast-color);
28 | background-color: var(--accent-color);
29 | }
30 |
31 | &:first-child {
32 | margin-left: 0;
33 | }
34 |
35 | &:last-child {
36 | margin-right: 0;
37 | }
38 |
39 | @media (max-width: 575px) {
40 | font-size: 14px;
41 | margin: 0 0 10px 0;
42 | }
43 | }
44 | }
45 |
46 | .portfolio-item {
47 | position: relative;
48 | overflow: hidden;
49 |
50 | .portfolio-info {
51 | opacity: 0;
52 | position: absolute;
53 | left: 12px;
54 | right: 12px;
55 | bottom: -100%;
56 | z-index: 3;
57 | transition: all ease-in-out 0.5s;
58 | background: color-mix(in srgb, var(--surface-color), transparent 10%);
59 | padding: 15px;
60 |
61 | h4 {
62 | font-size: 18px;
63 | font-weight: 600;
64 | padding-right: 50px;
65 | }
66 |
67 | p {
68 | color: color-mix(in srgb, var(--default-color), transparent 30%);
69 | font-size: 14px;
70 | margin-bottom: 0;
71 | padding-right: 50px;
72 | }
73 |
74 | .preview-link,
75 | .details-link {
76 | position: absolute;
77 | right: 50px;
78 | font-size: 24px;
79 | top: calc(50% - 14px);
80 | color: color-mix(in srgb, var(--default-color), transparent 30%);
81 | transition: 0.3s;
82 | line-height: 0;
83 |
84 | &:hover {
85 | color: var(--accent-color);
86 | }
87 | }
88 |
89 | .details-link {
90 | right: 14px;
91 | font-size: 28px;
92 | }
93 | }
94 |
95 | &:hover {
96 | .portfolio-info {
97 | opacity: 1;
98 | bottom: 0;
99 | }
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/store/header.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags, tagList],
3 | )
4 |
5 | #' Page header
6 | #'
7 | #' @param active String. The active page. See [nav()].
8 | #' @export
9 | header <- \(active = NULL) {
10 | tags$header(
11 | id = "header",
12 | class = "header d-flex align-items-center light-background sticky-top",
13 | tags$div(
14 | class = "container-fluid position-relative d-flex align-items-center justify-content-between",
15 | tags$a(
16 | href = "/",
17 | class = "logo d-flex align-items-center me-auto me-xl-0",
18 | # use an image logo?
19 | # tags$img(
20 | # src = "assets/img/logo.png",
21 | # alt = ""
22 | # )
23 | tags$h1(
24 | class = "sitename",
25 | "Mwavu"
26 | )
27 | ),
28 | nav(active = active),
29 | social_links(class = "header-social-links")
30 | )
31 | )
32 | }
33 |
34 | #' Page nav bar
35 | #'
36 | #' @param active String. The active page. Must be one of:
37 | #' - Home
38 | #' - About
39 | #' - Resume
40 | #' - Services
41 | #' - Portfolio
42 | #' - Contact
43 | #' @export
44 | nav <- \(active = NULL) {
45 | page_hrefs <- c(
46 | "/",
47 | "/about",
48 | "/resume",
49 | "/services",
50 | "/contact"
51 | )
52 | page_labels <- c(
53 | "Home",
54 | "About",
55 | "Resume",
56 | "Services",
57 | "Contact"
58 | )
59 | page_list_items <- Map(
60 | f = \(href, label) {
61 | tags$li(
62 | tags$a(
63 | href = href,
64 | class = if (identical(label, active)) "active",
65 | label
66 | )
67 | )
68 | },
69 | page_hrefs,
70 | page_labels
71 | )
72 |
73 | tags$nav(
74 | id = "navmenu",
75 | class = "navmenu",
76 | tags$ul(page_list_items),
77 | tags$i(class = "mobile-nav-toggle d-xl-none bi bi-list")
78 | )
79 | }
80 |
81 | #' Social links
82 | #'
83 | #' @param class Character vector. Classes to apply to the container div of the
84 | #' links.
85 | #' @export
86 | social_links <- \(class = NULL) {
87 | hrefs <- c(
88 | "https://x.com/kennedymwavu",
89 | "https://github.com/kennedymwavu",
90 | "https://www.linkedin.com/in/kennedymwavu/"
91 | )
92 | names <- c("twitter", "github", "linkedin")
93 | icons <- paste0(
94 | "bi bi-",
95 | c("twitter-x", "github", "linkedin")
96 | )
97 |
98 | links <- Map(
99 | f = \(href, name, icon) {
100 | tags$a(
101 | href = href,
102 | class = name,
103 | tags$i(class = icon)
104 | )
105 | },
106 | hrefs,
107 | names,
108 | icons
109 | )
110 |
111 | tags$div(
112 | class = class,
113 | links
114 | )
115 | }
116 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Personal Website
2 |
3 | Made with ❤️ using Ambiorix
4 |
5 | ## Prerequisites
6 |
7 | - R >= 4.5.0
8 |
9 | ## Installation
10 |
11 | 1. Clone the repo and `cd` into it:
12 |
13 | ```bash
14 | git@github.com:kennedymwavu/mwavu.git
15 | ```
16 |
17 | ```bash
18 | cd mwavu
19 | ```
20 |
21 | 1. Create an `env` file (`.Renviron`) at the root dir of the project and add this variable:
22 |
23 | ```r
24 | RENV_CONFIG_SANDBOX_ENABLED = FALSE
25 | ```
26 |
27 | 1. Restore package dependencies:
28 |
29 | ```bash
30 | R -e "renv::restore()"
31 | ```
32 |
33 | The `-e` flag tells R to execute that expression and exit.
34 |
35 | ## Run app
36 |
37 | ```r
38 | Rscript index.R
39 | ```
40 |
41 | Then visit [localhost:8000](http://localhost:8000/) to view the app.
42 |
43 | ## Contact Page Messages
44 |
45 | I have used [postmark](https://postmarkapp.com/)
46 | to forward the messages entered in the contact form
47 | to my email.
48 |
49 | You can use whichever mail server you prefer, but if
50 | you need the form to work as-is, you will have to:
51 |
52 | 1. Have a mailbox specific to your domain eg.
53 | 1. Create & configure a [postmark](https://postmarkapp.com/) account.
54 | 1. Add these variables to your `.Renviron`:
55 |
56 | ```r
57 | EMAIL = you@your-domain.com
58 | POSTMARK_SERVER_TOKEN = your-postmark-server-token
59 | ```
60 |
61 | ## Docker
62 |
63 | ### Initial Setup or Library/Package Updates
64 |
65 | If this is the first time running the project, or if library/package
66 | dependencies have changed, build the base image first:
67 |
68 | ```bash
69 | docker build -f Dockerfile.base -t personal-website-base .
70 | ```
71 |
72 | ### Redeploying After Code Changes
73 |
74 | 1. Generate the main `Dockerfile`:
75 |
76 | ```bash
77 | Rscript generate_dockerfile.R
78 | ```
79 |
80 | Generate this file locally so that changes are tracked via Git and only a
81 | `git pull` is needed on the server.
82 |
83 | Ensure you do not have any unnecessary files (eg. `dev.R`, `test.R` etc)
84 | when running this to avoid "file not found" errors on the server. But
85 | if you have such files (which you probably use for development), just
86 | edit the `generate_dockerfile.R` and add them to the vector of ignored
87 | prefixes. That way, they will not be `COPY`ied.
88 |
89 | 2. Build the main image:
90 |
91 | ```bash
92 | docker build -t personal-website .
93 | ```
94 |
95 | 3. Stop the currently running services in this context:
96 |
97 | ```bash
98 | docker compose down
99 | ```
100 |
101 | 4. Start services with updates:
102 |
103 | ```bash
104 | docker compose up -d --remove-orphans
105 | ```
106 |
107 | This will run the app on port 1028 of the host machine, so you will view it
108 | at [localhost:1028](http://localhost:1028/)
109 |
--------------------------------------------------------------------------------
/public/scss/_variables.scss:
--------------------------------------------------------------------------------
1 | // main: main.scss
2 | /*--------------------------------------------------------------
3 | # Font & Color Variables
4 | --------------------------------------------------------------*/
5 |
6 | /* Fonts */
7 | :root {
8 | --default-font: "Roboto", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
9 | --heading-font: "Raleway", sans-serif;
10 | --nav-font: "Poppins", sans-serif;
11 | }
12 |
13 | /* Global Colors - The following color variables are used throughout the website. Updating them here will change the color scheme of the entire website */
14 | :root {
15 | --background-color: #ffffff; /* Background color for the entire website, including individual sections */
16 | --default-color: #444444; /* Default color used for the majority of the text content across the entire website */
17 | --heading-color: #222222; /* Color for headings, subheadings and title throughout the website */
18 | --accent-color: #34b7a7; /* Accent color that represents your brand on the website. It's used for buttons, links, and other elements that need to stand out */
19 | --surface-color: #ffffff; /* The surface color is used as a background of boxed elements within sections, such as cards, icon boxes, or other elements that require a visual separation from the global background. */
20 | --contrast-color: #ffffff; /* Contrast color for text, ensuring readability against backgrounds of accent, heading, or default colors. */
21 | }
22 |
23 | /* Nav Menu Colors - The following color variables are used specifically for the navigation menu. They are separate from the global colors to allow for more customization options */
24 | :root {
25 | --nav-color: #444444; /* The default color of the main navmenu links */
26 | --nav-hover-color: #34b7a7; /* Applied to main navmenu links when they are hovered over or active */
27 | --nav-mobile-background-color: #ffffff; /* Used as the background color for mobile navigation menu */
28 | --nav-dropdown-background-color: #ffffff; /* Used as the background color for dropdown items that appear when hovering over primary navigation items */
29 | --nav-dropdown-color: #444444; /* Used for navigation links of the dropdown items in the navigation menu. */
30 | --nav-dropdown-hover-color: #34b7a7; /* Similar to --nav-hover-color, this color is applied to dropdown navigation links when they are hovered over. */
31 | }
32 |
33 | /* Color Presets - These classes override global colors when applied to any section or element, providing reuse of the sam color scheme. */
34 |
35 | .light-background {
36 | --background-color: #e9e8e6;
37 | --surface-color: #ffffff;
38 | }
39 |
40 | .dark-background {
41 | --background-color: #060606;
42 | --default-color: #ffffff;
43 | --heading-color: #ffffff;
44 | --surface-color: #252525;
45 | --contrast-color: #ffffff;
46 | }
47 |
48 | /* Smooth scroll */
49 | :root {
50 | scroll-behavior: smooth;
51 | }
52 |
--------------------------------------------------------------------------------
/controllers/contact/validations.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags],
3 | .. /
4 | .. /
5 | store /
6 | mod[
7 | text_input,
8 | text_area_input,
9 | ],
10 | .. / .. / helpers / operators[`%||%`],
11 | )
12 |
13 | #' Validate name
14 | #'
15 | #' @param name String. Name.
16 | #' @return Named list:
17 | #' - `ok`: Logical. Did all name validations pass?
18 | #' - `value`: The name.
19 | #' - `validations`: [htmltools::tags] The validated tags.
20 | #' @export
21 | validate_name <- \(name) {
22 | name <- name %||% ""
23 | ok <- nzchar(name)
24 |
25 | input_class <- "is-valid"
26 | validations <- NULL
27 |
28 | if (!ok) {
29 | input_class <- "is-invalid"
30 | validations <- tags$div(
31 | class = "invalid-feedback",
32 | "Please enter your name"
33 | )
34 | }
35 |
36 | list(
37 | ok = ok,
38 | value = name,
39 | validations = text_input(
40 | validations,
41 | label = "Your Name",
42 | id = "name",
43 | value = name,
44 | class = input_class
45 | )
46 | )
47 | }
48 |
49 | #' Validate email
50 | #'
51 | #' @param email String. email.
52 | #' @return Named list:
53 | #' - `ok`: Logical. Is email okay?
54 | #' - `validations`: [htmltools::tags] The validated tags.
55 | #' - `value`: The email.
56 | #' @export
57 | validate_email <- \(email) {
58 | email <- email %||% ""
59 | ok <- grepl(
60 | pattern = "\\<[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}\\>",
61 | x = email,
62 | ignore.case = TRUE
63 | )
64 |
65 | input_class <- "is-valid"
66 | validations <- NULL
67 |
68 | if (!ok) {
69 | input_class <- "is-invalid"
70 |
71 | validations <- tags$div(
72 | class = "invalid-feedback",
73 | "Please provide a valid email!"
74 | )
75 | }
76 |
77 | list(
78 | ok = ok,
79 | value = email,
80 | validations = text_input(
81 | validations,
82 | label = "Your Email",
83 | id = "email",
84 | type = "email",
85 | value = email,
86 | class = input_class
87 | )
88 | )
89 | }
90 |
91 | #' Validate subject
92 | #'
93 | #' @param subject String. Subject.
94 | #' @return Named list:
95 | #' - `ok`: Logical. Did all validations pass?
96 | #' - `value`: The subject.
97 | #' - `validations`: [htmltools::tags] The validated tags.
98 | #' @export
99 | validate_subject <- \(subject) {
100 | subject <- subject %||% ""
101 | ok <- nzchar(subject)
102 |
103 | input_class <- "is-valid"
104 | validations <- NULL
105 |
106 | if (!ok) {
107 | input_class <- "is-invalid"
108 | validations <- tags$div(
109 | class = "invalid-feedback",
110 | "Please enter the subject"
111 | )
112 | }
113 |
114 | list(
115 | ok = ok,
116 | value = subject,
117 | validations = text_input(
118 | validations,
119 | label = "Subject",
120 | id = "subject",
121 | value = subject,
122 | class = input_class
123 | )
124 | )
125 | }
126 |
127 | #' Validate message
128 | #'
129 | #' @param message String. Message.
130 | #' @return Named list:
131 | #' - `ok`: Logical. Did all validations pass?
132 | #' - `value`: The message.
133 | #' - `validations`: [htmltools::tags] The validated tags.
134 | #' @export
135 | validate_message <- \(message) {
136 | message <- message %||% ""
137 | ok <- nzchar(message)
138 |
139 | input_class <- "is-valid"
140 | validations <- NULL
141 |
142 | if (!ok) {
143 | input_class <- "is-invalid"
144 | validations <- tags$div(
145 | class = "invalid-feedback",
146 | "Please enter a message"
147 | )
148 | }
149 |
150 | list(
151 | ok = ok,
152 | value = message,
153 | validations = text_area_input(
154 | validations,
155 | label = "Message",
156 | id = "message",
157 | value = message,
158 | class = input_class
159 | )
160 | )
161 | }
162 |
--------------------------------------------------------------------------------
/public/img/skills/openapi.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vendor/purecounter/purecounter_vanilla.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * purecounter.js - A simple yet configurable native javascript counter which you can count on.
3 | * Author: Stig Rex
4 | * Version: 1.5.0
5 | * Url: https://github.com/srexi/purecounterjs
6 | * License: MIT
7 | */
8 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.PureCounter=t():e.PureCounter=t()}(self,(function(){return e={638:function(e){function t(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function r(e){return function(e){if(Array.isArray(e))return n(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(e){if("string"==typeof e)return n(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);return"Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?n(e,t):void 0}}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function n(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r1&&void 0!==arguments[1]?arguments[1]:{},r={};for(var n in e)if(t=={}||t.hasOwnProperty(n)){var o=c(e[n]);r[n]=o,n.match(/duration|pulse/)&&(r[n]="boolean"!=typeof o?1e3*o:o)}return Object.assign({},t,r)}function i(e,t){var r=(t.end-t.start)/(t.duration/t.delay),n="inc";t.start>t.end&&(n="dec",r*=-1);var o=c(t.start);e.innerHTML=u(o,t),!0===t.once&&e.setAttribute("data-purecounter-duration",0);var i=setInterval((function(){var a=function(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"inc";return e=c(e),t=c(t),parseFloat("inc"===r?e+t:e-t)}(o,r,n);e.innerHTML=u(a,t),((o=a)>=t.end&&"inc"==n||o<=t.end&&"dec"==n)&&(e.innerHTML=u(t.end,t),t.pulse&&(e.setAttribute("data-purecounter-duration",0),setTimeout((function(){e.setAttribute("data-purecounter-duration",t.duration/1e3)}),t.pulse)),clearInterval(i))}),t.delay)}function a(e,t){return Math.pow(e,t)}function u(e,t){var r={minimumFractionDigits:t.decimals,maximumFractionDigits:t.decimals},n="string"==typeof t.formater?t.formater:void 0;return e=function(e,t){if(t.filesizing||t.currency){e=Math.abs(Number(e));var r=1e3,n=t.currency&&"string"==typeof t.currency?t.currency:"",o=t.decimals||1,i=["","K","M","B","T"],u="";t.filesizing&&(r=1024,i=["bytes","KB","MB","GB","TB"]);for(var c=4;c>=0;c--)if(0===c&&(u="".concat(e.toFixed(o)," ").concat(i[c])),e>=a(r,c)){u="".concat((e/a(r,c)).toFixed(o)," ").concat(i[c]);break}return n+u}return parseFloat(e)}(e,t),function(e,t){if(t.formater){var r=t.separator?"string"==typeof t.separator?t.separator:",":"";return"en-US"!==t.formater&&!0===t.separator?e:(n=r,e.replace(/^(?:(\d{1,3},(?:\d{1,3},?)*)|(\d{1,3}\.(?:\d{1,3}\.?)*)|(\d{1,3}(?:\s\d{1,3})*))([\.,]?\d{0,2}?)$/gi,(function(e,t,r,o,i){var a="",u="";if(void 0!==t?(a=t.replace(new RegExp(/,/gi,"gi"),n),u=","):void 0!==r?a=r.replace(new RegExp(/\./gi,"gi"),n):void 0!==o&&(a=o.replace(new RegExp(/ /gi,"gi"),n)),void 0!==i){var c=","!==u&&","!==n?",":".";a+=void 0!==i?i.replace(new RegExp(/\.|,/gi,"gi"),c):""}return a})))}var n;return e}(e=t.formater?e.toLocaleString(n,r):parseInt(e).toString(),t)}function c(e){return/^[0-9]+\.[0-9]+$/.test(e)?parseFloat(e):/^[0-9]+$/.test(e)?parseInt(e):/^true|false/i.test(e)?/^true/i.test(e):e}function f(e){for(var t=e.offsetTop,r=e.offsetLeft,n=e.offsetWidth,o=e.offsetHeight;e.offsetParent;)t+=(e=e.offsetParent).offsetTop,r+=e.offsetLeft;return t>=window.pageYOffset&&r>=window.pageXOffset&&t+o<=window.pageYOffset+window.innerHeight&&r+n<=window.pageXOffset+window.innerWidth}function s(){return"IntersectionObserver"in window&&"IntersectionObserverEntry"in window&&"intersectionRatio"in window.IntersectionObserverEntry.prototype}e.exports=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n={start:0,end:100,duration:2e3,delay:10,once:!0,pulse:!1,decimals:0,legacy:!0,filesizing:!1,currency:!1,separator:!1,formater:"us-US",selector:".purecounter"},a=o(e,n);function d(){var e=document.querySelectorAll(a.selector);if(0!==e.length)if(s()){var t=new IntersectionObserver(p.bind(this),{root:null,rootMargin:"20px",threshold:.5});e.forEach((function(e){t.observe(e)}))}else window.addEventListener&&(l(e),window.addEventListener("scroll",(function(t){l(e)}),{passive:!0}))}function l(e){e.forEach((function(e){!0===v(e).legacy&&f(e)&&p([e])}))}function p(e,t){e.forEach((function(e){var r=e.target||e,n=v(r);if(n.duration<=0)return r.innerHTML=u(n.end,n);if(!t&&!f(e)||t&&e.intersectionRatio<.5){var o=n.start>n.end?n.end:n.start;return r.innerHTML=u(o,n)}setTimeout((function(){return i(r,n)}),n.delay)}))}function v(e){var n=a,i=[].filter.call(e.attributes,(function(e){return/^data-purecounter-/.test(e.name)}));return o(0!=i.length?Object.assign.apply(Object,[{}].concat(r(i.map((function(e){var r=e.name,n=e.value;return t({},r.replace("data-purecounter-","").toLowerCase(),c(n))}))))):{},n)}d()}}},t={},r=function r(n){var o=t[n];if(void 0!==o)return o.exports;var i=t[n]={exports:{}};return e[n](i,i.exports,r),i.exports}(638),r;var e,t,r}));
9 | //# sourceMappingURL=purecounter_vanilla.js.map
--------------------------------------------------------------------------------
/public/vendor/imagesloaded/imagesloaded.pkgd.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * imagesLoaded PACKAGED v5.0.0
3 | * JavaScript is all like "You images are done yet or what?"
4 | * MIT License
5 | */
6 | !function(t,e){"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,(function(){function t(){}let e=t.prototype;return e.on=function(t,e){if(!t||!e)return this;let i=this._events=this._events||{},s=i[t]=i[t]||[];return s.includes(e)||s.push(e),this},e.once=function(t,e){if(!t||!e)return this;this.on(t,e);let i=this._onceEvents=this._onceEvents||{};return(i[t]=i[t]||{})[e]=!0,this},e.off=function(t,e){let i=this._events&&this._events[t];if(!i||!i.length)return this;let s=i.indexOf(e);return-1!=s&&i.splice(s,1),this},e.emitEvent=function(t,e){let i=this._events&&this._events[t];if(!i||!i.length)return this;i=i.slice(0),e=e||[];let s=this._onceEvents&&this._onceEvents[t];for(let n of i){s&&s[n]&&(this.off(t,n),delete s[n]),n.apply(this,e)}return this},e.allOff=function(){return delete this._events,delete this._onceEvents,this},t})),
7 | /*!
8 | * imagesLoaded v5.0.0
9 | * JavaScript is all like "You images are done yet or what?"
10 | * MIT License
11 | */
12 | function(t,e){"object"==typeof module&&module.exports?module.exports=e(t,require("ev-emitter")):t.imagesLoaded=e(t,t.EvEmitter)}("undefined"!=typeof window?window:this,(function(t,e){let i=t.jQuery,s=t.console;function n(t,e,o){if(!(this instanceof n))return new n(t,e,o);let r=t;var h;("string"==typeof t&&(r=document.querySelectorAll(t)),r)?(this.elements=(h=r,Array.isArray(h)?h:"object"==typeof h&&"number"==typeof h.length?[...h]:[h]),this.options={},"function"==typeof e?o=e:Object.assign(this.options,e),o&&this.on("always",o),this.getImages(),i&&(this.jqDeferred=new i.Deferred),setTimeout(this.check.bind(this))):s.error(`Bad element for imagesLoaded ${r||t}`)}n.prototype=Object.create(e.prototype),n.prototype.getImages=function(){this.images=[],this.elements.forEach(this.addElementImages,this)};const o=[1,9,11];n.prototype.addElementImages=function(t){"IMG"===t.nodeName&&this.addImage(t),!0===this.options.background&&this.addElementBackgroundImages(t);let{nodeType:e}=t;if(!e||!o.includes(e))return;let i=t.querySelectorAll("img");for(let t of i)this.addImage(t);if("string"==typeof this.options.background){let e=t.querySelectorAll(this.options.background);for(let t of e)this.addElementBackgroundImages(t)}};const r=/url\((['"])?(.*?)\1\)/gi;function h(t){this.img=t}function d(t,e){this.url=t,this.element=e,this.img=new Image}return n.prototype.addElementBackgroundImages=function(t){let e=getComputedStyle(t);if(!e)return;let i=r.exec(e.backgroundImage);for(;null!==i;){let s=i&&i[2];s&&this.addBackground(s,t),i=r.exec(e.backgroundImage)}},n.prototype.addImage=function(t){let e=new h(t);this.images.push(e)},n.prototype.addBackground=function(t,e){let i=new d(t,e);this.images.push(i)},n.prototype.check=function(){if(this.progressedCount=0,this.hasAnyBroken=!1,!this.images.length)return void this.complete();let t=(t,e,i)=>{setTimeout((()=>{this.progress(t,e,i)}))};this.images.forEach((function(e){e.once("progress",t),e.check()}))},n.prototype.progress=function(t,e,i){this.progressedCount++,this.hasAnyBroken=this.hasAnyBroken||!t.isLoaded,this.emitEvent("progress",[this,t,e]),this.jqDeferred&&this.jqDeferred.notify&&this.jqDeferred.notify(this,t),this.progressedCount===this.images.length&&this.complete(),this.options.debug&&s&&s.log(`progress: ${i}`,t,e)},n.prototype.complete=function(){let t=this.hasAnyBroken?"fail":"done";if(this.isComplete=!0,this.emitEvent(t,[this]),this.emitEvent("always",[this]),this.jqDeferred){let t=this.hasAnyBroken?"reject":"resolve";this.jqDeferred[t](this)}},h.prototype=Object.create(e.prototype),h.prototype.check=function(){this.getIsImageComplete()?this.confirm(0!==this.img.naturalWidth,"naturalWidth"):(this.proxyImage=new Image,this.img.crossOrigin&&(this.proxyImage.crossOrigin=this.img.crossOrigin),this.proxyImage.addEventListener("load",this),this.proxyImage.addEventListener("error",this),this.img.addEventListener("load",this),this.img.addEventListener("error",this),this.proxyImage.src=this.img.currentSrc||this.img.src)},h.prototype.getIsImageComplete=function(){return this.img.complete&&this.img.naturalWidth},h.prototype.confirm=function(t,e){this.isLoaded=t;let{parentNode:i}=this.img,s="PICTURE"===i.nodeName?i:this.img;this.emitEvent("progress",[this,s,e])},h.prototype.handleEvent=function(t){let e="on"+t.type;this[e]&&this[e](t)},h.prototype.onload=function(){this.confirm(!0,"onload"),this.unbindEvents()},h.prototype.onerror=function(){this.confirm(!1,"onerror"),this.unbindEvents()},h.prototype.unbindEvents=function(){this.proxyImage.removeEventListener("load",this),this.proxyImage.removeEventListener("error",this),this.img.removeEventListener("load",this),this.img.removeEventListener("error",this)},d.prototype=Object.create(h.prototype),d.prototype.check=function(){this.img.addEventListener("load",this),this.img.addEventListener("error",this),this.img.src=this.url,this.getIsImageComplete()&&(this.confirm(0!==this.img.naturalWidth,"naturalWidth"),this.unbindEvents())},d.prototype.unbindEvents=function(){this.img.removeEventListener("load",this),this.img.removeEventListener("error",this)},d.prototype.confirm=function(t,e){this.isLoaded=t,this.emitEvent("progress",[this,this.element,e])},n.makeJQueryPlugin=function(e){(e=e||t.jQuery)&&(i=e,i.fn.imagesLoaded=function(t,e){return new n(this,t,e).jqDeferred.promise(i(this))})},n.makeJQueryPlugin(),n}));
--------------------------------------------------------------------------------
/public/vendor/toastr/toastr-2.1.3.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Note that this is toastr v2.1.3, the "latest" version in url has no more maintenance,
3 | * please go to https://cdnjs.com/libraries/toastr.js and pick a certain version you want to use,
4 | * make sure you copy the url from the website since the url may change between versions.
5 | * */
6 | !function(e){e(["jquery"],function(e){return function(){function t(e,t,n){return g({type:O.error,iconClass:m().iconClasses.error,message:e,optionsOverride:n,title:t})}function n(t,n){return t||(t=m()),v=e("#"+t.containerId),v.length?v:(n&&(v=d(t)),v)}function o(e,t,n){return g({type:O.info,iconClass:m().iconClasses.info,message:e,optionsOverride:n,title:t})}function s(e){C=e}function i(e,t,n){return g({type:O.success,iconClass:m().iconClasses.success,message:e,optionsOverride:n,title:t})}function a(e,t,n){return g({type:O.warning,iconClass:m().iconClasses.warning,message:e,optionsOverride:n,title:t})}function r(e,t){var o=m();v||n(o),u(e,o,t)||l(o)}function c(t){var o=m();return v||n(o),t&&0===e(":focus",t).length?void h(t):void(v.children().length&&v.remove())}function l(t){for(var n=v.children(),o=n.length-1;o>=0;o--)u(e(n[o]),t)}function u(t,n,o){var s=!(!o||!o.force)&&o.force;return!(!t||!s&&0!==e(":focus",t).length)&&(t[n.hideMethod]({duration:n.hideDuration,easing:n.hideEasing,complete:function(){h(t)}}),!0)}function d(t){return v=e("").attr("id",t.containerId).addClass(t.positionClass),v.appendTo(e(t.target)),v}function p(){return{tapToDismiss:!0,toastClass:"toast",containerId:"toast-container",debug:!1,showMethod:"fadeIn",showDuration:300,showEasing:"swing",onShown:void 0,hideMethod:"fadeOut",hideDuration:1e3,hideEasing:"swing",onHidden:void 0,closeMethod:!1,closeDuration:!1,closeEasing:!1,closeOnHover:!0,extendedTimeOut:1e3,iconClasses:{error:"toast-error",info:"toast-info",success:"toast-success",warning:"toast-warning"},iconClass:"toast-info",positionClass:"toast-top-right",timeOut:5e3,titleClass:"toast-title",messageClass:"toast-message",escapeHtml:!1,target:"body",closeHtml:'',closeClass:"toast-close-button",newestOnTop:!0,preventDuplicates:!1,progressBar:!1,progressClass:"toast-progress",rtl:!1}}function f(e){C&&C(e)}function g(t){function o(e){return null==e&&(e=""),e.replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function s(){c(),u(),d(),p(),g(),C(),l(),i()}function i(){var e="";switch(t.iconClass){case"toast-success":case"toast-info":e="polite";break;default:e="assertive"}I.attr("aria-live",e)}function a(){E.closeOnHover&&I.hover(H,D),!E.onclick&&E.tapToDismiss&&I.click(b),E.closeButton&&j&&j.click(function(e){e.stopPropagation?e.stopPropagation():void 0!==e.cancelBubble&&e.cancelBubble!==!0&&(e.cancelBubble=!0),E.onCloseClick&&E.onCloseClick(e),b(!0)}),E.onclick&&I.click(function(e){E.onclick(e),b()})}function r(){I.hide(),I[E.showMethod]({duration:E.showDuration,easing:E.showEasing,complete:E.onShown}),E.timeOut>0&&(k=setTimeout(b,E.timeOut),F.maxHideTime=parseFloat(E.timeOut),F.hideEta=(new Date).getTime()+F.maxHideTime,E.progressBar&&(F.intervalId=setInterval(x,10)))}function c(){t.iconClass&&I.addClass(E.toastClass).addClass(y)}function l(){E.newestOnTop?v.prepend(I):v.append(I)}function u(){if(t.title){var e=t.title;E.escapeHtml&&(e=o(t.title)),M.append(e).addClass(E.titleClass),I.append(M)}}function d(){if(t.message){var e=t.message;E.escapeHtml&&(e=o(t.message)),B.append(e).addClass(E.messageClass),I.append(B)}}function p(){E.closeButton&&(j.addClass(E.closeClass).attr("role","button"),I.prepend(j))}function g(){E.progressBar&&(q.addClass(E.progressClass),I.prepend(q))}function C(){E.rtl&&I.addClass("rtl")}function O(e,t){if(e.preventDuplicates){if(t.message===w)return!0;w=t.message}return!1}function b(t){var n=t&&E.closeMethod!==!1?E.closeMethod:E.hideMethod,o=t&&E.closeDuration!==!1?E.closeDuration:E.hideDuration,s=t&&E.closeEasing!==!1?E.closeEasing:E.hideEasing;if(!e(":focus",I).length||t)return clearTimeout(F.intervalId),I[n]({duration:o,easing:s,complete:function(){h(I),clearTimeout(k),E.onHidden&&"hidden"!==P.state&&E.onHidden(),P.state="hidden",P.endTime=new Date,f(P)}})}function D(){(E.timeOut>0||E.extendedTimeOut>0)&&(k=setTimeout(b,E.extendedTimeOut),F.maxHideTime=parseFloat(E.extendedTimeOut),F.hideEta=(new Date).getTime()+F.maxHideTime)}function H(){clearTimeout(k),F.hideEta=0,I.stop(!0,!0)[E.showMethod]({duration:E.showDuration,easing:E.showEasing})}function x(){var e=(F.hideEta-(new Date).getTime())/F.maxHideTime*100;q.width(e+"%")}var E=m(),y=t.iconClass||E.iconClass;if("undefined"!=typeof t.optionsOverride&&(E=e.extend(E,t.optionsOverride),y=t.optionsOverride.iconClass||y),!O(E,t)){T++,v=n(E,!0);var k=null,I=e(""),M=e(""),B=e(""),q=e(""),j=e(E.closeHtml),F={intervalId:null,hideEta:null,maxHideTime:null},P={toastId:T,state:"visible",startTime:new Date,options:E,map:t};return s(),r(),a(),f(P),E.debug&&console&&console.log(P),I}}function m(){return e.extend({},p(),b.options)}function h(e){v||(v=n()),e.is(":visible")||(e.remove(),e=null,0===v.children().length&&(v.remove(),w=void 0))}var v,C,w,T=0,O={error:"error",info:"info",success:"success",warning:"warning"},b={clear:r,remove:c,error:t,getContainer:n,info:o,options:{},subscribe:s,success:i,version:"2.1.3",warning:a};return b}()})}("function"==typeof define&&define.amd?define:function(e,t){"undefined"!=typeof module&&module.exports?module.exports=t(require("jquery")):window.toastr=t(window.jQuery)});
7 | //# sourceMappingURL=toastr.js.map
8 |
--------------------------------------------------------------------------------
/public/js/main.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | "use strict";
3 |
4 | /**
5 | * Apply .scrolled class to the body as the page is scrolled down
6 | */
7 | function toggleScrolled() {
8 | const selectBody = document.querySelector('body');
9 | const selectHeader = document.querySelector('#header');
10 | if (!selectHeader.classList.contains('scroll-up-sticky') && !selectHeader.classList.contains('sticky-top') && !selectHeader.classList.contains('fixed-top')) return;
11 | window.scrollY > 100 ? selectBody.classList.add('scrolled') : selectBody.classList.remove('scrolled');
12 | }
13 |
14 | document.addEventListener('scroll', toggleScrolled);
15 | window.addEventListener('load', toggleScrolled);
16 |
17 | /**
18 | * Mobile nav toggle
19 | */
20 | const mobileNavToggleBtn = document.querySelector('.mobile-nav-toggle');
21 |
22 | function mobileNavToogle() {
23 | document.querySelector('body').classList.toggle('mobile-nav-active');
24 | mobileNavToggleBtn.classList.toggle('bi-list');
25 | mobileNavToggleBtn.classList.toggle('bi-x');
26 | }
27 | mobileNavToggleBtn.addEventListener('click', mobileNavToogle);
28 |
29 | /**
30 | * Hide mobile nav on same-page/hash links
31 | */
32 | document.querySelectorAll('#navmenu a').forEach(navmenu => {
33 | navmenu.addEventListener('click', () => {
34 | if (document.querySelector('.mobile-nav-active')) {
35 | mobileNavToogle();
36 | }
37 | });
38 |
39 | });
40 |
41 | /**
42 | * Toggle mobile nav dropdowns
43 | */
44 | document.querySelectorAll('.navmenu .toggle-dropdown').forEach(navmenu => {
45 | navmenu.addEventListener('click', function(e) {
46 | e.preventDefault();
47 | this.parentNode.classList.toggle('active');
48 | this.parentNode.nextElementSibling.classList.toggle('dropdown-active');
49 | e.stopImmediatePropagation();
50 | });
51 | });
52 |
53 | /**
54 | * Preloader
55 | */
56 | const preloader = document.querySelector('#preloader');
57 | if (preloader) {
58 | window.addEventListener('load', () => {
59 | preloader.remove();
60 | });
61 | }
62 |
63 | /**
64 | * Scroll top button
65 | */
66 | let scrollTop = document.querySelector('.scroll-top');
67 |
68 | function toggleScrollTop() {
69 | if (scrollTop) {
70 | window.scrollY > 100 ? scrollTop.classList.add('active') : scrollTop.classList.remove('active');
71 | }
72 | }
73 | scrollTop.addEventListener('click', (e) => {
74 | e.preventDefault();
75 | window.scrollTo({
76 | top: 0,
77 | behavior: 'smooth'
78 | });
79 | });
80 |
81 | window.addEventListener('load', toggleScrollTop);
82 | document.addEventListener('scroll', toggleScrollTop);
83 |
84 | /**
85 | * Animation on scroll function and init
86 | */
87 | function aosInit() {
88 | AOS.init({
89 | duration: 600,
90 | easing: 'ease-in-out',
91 | once: true,
92 | mirror: false
93 | });
94 | }
95 | window.addEventListener('load', aosInit);
96 |
97 | /**
98 | * Animate the skills items on reveal
99 | */
100 | let skillsAnimation = document.querySelectorAll('.skills-animation');
101 | skillsAnimation.forEach((item) => {
102 | new Waypoint({
103 | element: item,
104 | offset: '80%',
105 | handler: function(direction) {
106 | let progress = item.querySelectorAll('.progress .progress-bar');
107 | progress.forEach(el => {
108 | el.style.width = el.getAttribute('aria-valuenow') + '%';
109 | });
110 | }
111 | });
112 | });
113 |
114 | /**
115 | * Initiate Pure Counter
116 | */
117 | new PureCounter();
118 |
119 | /**
120 | * Init swiper sliders
121 | */
122 | function initSwiper() {
123 | document.querySelectorAll(".init-swiper").forEach(function(swiperElement) {
124 | let config = JSON.parse(
125 | swiperElement.querySelector(".swiper-config").innerHTML.trim()
126 | );
127 |
128 | if (swiperElement.classList.contains("swiper-tab")) {
129 | initSwiperWithCustomPagination(swiperElement, config);
130 | } else {
131 | new Swiper(swiperElement, config);
132 | }
133 | });
134 | }
135 |
136 | window.addEventListener("load", initSwiper);
137 |
138 | /**
139 | * Initiate glightbox
140 | */
141 | const glightbox = GLightbox({
142 | selector: '.glightbox'
143 | });
144 |
145 | /**
146 | * Init isotope layout and filters
147 | */
148 | document.querySelectorAll('.isotope-layout').forEach(function(isotopeItem) {
149 | let layout = isotopeItem.getAttribute('data-layout') ?? 'masonry';
150 | let filter = isotopeItem.getAttribute('data-default-filter') ?? '*';
151 | let sort = isotopeItem.getAttribute('data-sort') ?? 'original-order';
152 |
153 | let initIsotope;
154 | imagesLoaded(isotopeItem.querySelector('.isotope-container'), function() {
155 | initIsotope = new Isotope(isotopeItem.querySelector('.isotope-container'), {
156 | itemSelector: '.isotope-item',
157 | layoutMode: layout,
158 | filter: filter,
159 | sortBy: sort
160 | });
161 | });
162 |
163 | isotopeItem.querySelectorAll('.isotope-filters li').forEach(function(filters) {
164 | filters.addEventListener('click', function() {
165 | isotopeItem.querySelector('.isotope-filters .filter-active').classList.remove('filter-active');
166 | this.classList.add('filter-active');
167 | initIsotope.arrange({
168 | filter: this.getAttribute('data-filter')
169 | });
170 | if (typeof aosInit === 'function') {
171 | aosInit();
172 | }
173 | }, false);
174 | });
175 |
176 | });
177 |
178 | })();
179 |
--------------------------------------------------------------------------------
/store/resume.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags, tagList],
3 | . / header[header],
4 | . / create_section_title[create_section_title],
5 | )
6 |
7 | #' Resume page
8 | #'
9 | #' @return [htmltools::tags]
10 | #' @export
11 | resume <- \() {
12 | summary <- create_resume_item(
13 | title = "Kennedy Mwavu",
14 | institution = c(
15 | "I'm always aiming to make a tangible impact through my work. I seek out challenges that blend analytical thinking with creative problem-solving."
16 | ),
17 | items = c(
18 | "Programming Languages: R, JavaScript",
19 | "Version Control: Git, GitHub",
20 | "APIs: RESTful API design & development",
21 | "Containers: Docker",
22 | "Cloud Platforms: Google Cloud Platform, Digital Ocean"
23 | ),
24 | class = "pb-0"
25 | )
26 |
27 | university <- create_resume_item(
28 | title = "Bachelor's Degree",
29 | period = "Sept 2018 - Sept 2022",
30 | institution = "University of Nairobi",
31 | items = c(
32 | "BSc Actuarial Science"
33 | )
34 | )
35 |
36 | high_school <- create_resume_item(
37 | title = "Highschool Diploma",
38 | period = "Jan 2014 - Dec 2017",
39 | institution = "Alliance High School",
40 | items = c(
41 | "Kenya Certificate of Secondary Education (KCSE)"
42 | )
43 | )
44 |
45 | actserv <- create_resume_item(
46 | title = "Software Developer",
47 | period = "July 2021 - March 2025",
48 | institution = "Actuarial Services (EA) Ltd",
49 | items = c(
50 | "Revolutionized the efficiency of runoff triangle processing, cutting processing time from 2 hours to 10 seconds. This breakthrough significantly boosted analytical productivity and accuracy for client projects.",
51 | "Collaborated with the insurance team to develop comprehensive software modules for IFRS 17 insurance contracts, enhancing compliance, reporting accuracy, and operational efficiency.",
52 | "Designed RESTful APIs consumed by internal and external applications, ensuring secure and efficient data exchange when integrating with third-party services.",
53 | "Led a team of junior developers in the design, development and deployment of key software modules, providing mentorship and ensuring adherence to best coding practices.",
54 | "Developed automated data processing pipelines that reduced manual intervention, thereby minimizing errors and cutting down data processing time by over 80%."
55 | )
56 | )
57 |
58 | seven_skies <- create_resume_item(
59 | title = "Software Developer, Consultant",
60 | period = "Jan 2023 - June 2023",
61 | institution = "Seven Skies",
62 | items = c(
63 | "Implemented robust security measures, including encryption, authentication, and authorization protocols, safeguarding sensitive client data and ensuring compliance with industry standards.",
64 | "Transitioned legacy monolithic applications to a microservices architecture, improving system modularity, scalability and maintainability.",
65 | "Set up continuous integration and continuous deployment pipelines using GitHub Actions and Docker, enabling rapid and reliable application updates and deployments.",
66 | "Debugged and optimized R Shiny applications, improving performance and user experience.",
67 | "Deployed applications via Google Cloud Platform (GCP), ensuring seamless operation and scalability."
68 | )
69 | )
70 |
71 | download_link <- tags$a(
72 | class = "btn-download-resume",
73 | href = "/download-resume",
74 | target = "_blank",
75 | "Download My Resume"
76 | )
77 |
78 | tagList(
79 | header(active = "Resume"),
80 | tags$main(
81 | class = "main",
82 | tags$section(
83 | id = "resume",
84 | class = "resume section",
85 | create_section_title(
86 | title = "Resume",
87 | subtitle = "A snapshot of my professional growth & accomplishments"
88 | ),
89 | tags$div(
90 | class = "container",
91 | tags$div(
92 | class = "row",
93 | tags$div(
94 | class = "col-lg-6",
95 | `data-aos` = "fade-up",
96 | `data-aos-delay` = "100",
97 | tags$h3(
98 | class = "resume-title",
99 | "Summary"
100 | ),
101 | summary,
102 | tags$h3(
103 | class = "resume-title",
104 | "Education"
105 | ),
106 | university,
107 | high_school
108 | ),
109 | tags$div(
110 | class = "col-lg-6",
111 | `data-aos` = "fade-up",
112 | `data-aos-delay` = "200",
113 | tags$h3(
114 | class = "resume-title",
115 | "Professional Experience"
116 | ),
117 | actserv,
118 | seven_skies,
119 | tags$div(
120 | class = "text-center",
121 | download_link
122 | )
123 | )
124 | )
125 | )
126 | )
127 | )
128 | )
129 | }
130 |
131 | #' Create resume item
132 | #'
133 | #' @param title String. Title.
134 | #' @param period String. Period eg. "Sept 2018 - Sept 2022".
135 | #' @param institution String. Institution attended.
136 | #' @param items Character vector. Items to include.
137 | #' @param class Character vector. Classes to apply to the container div.
138 | #' @return [htmltools::tags]
139 | create_resume_item <- \(
140 | title = NULL,
141 | period = NULL,
142 | institution = NULL,
143 | items = NULL,
144 | class = NULL
145 | ) {
146 | class <- c("resume-item", class)
147 | title <- if (!is.null(title)) tags$h4(title)
148 | period <- if (!is.null(period)) tags$h5(period)
149 | institution <- if (!is.null(institution)) tags$p(tags$em(institution))
150 | items <- if (!is.null(items)) {
151 | tags$ul(
152 | lapply(items, \(item) {
153 | tags$li(item)
154 | })
155 | )
156 | }
157 |
158 | tags$div(
159 | class = class,
160 | title,
161 | period,
162 | institution,
163 | items
164 | )
165 | }
166 |
--------------------------------------------------------------------------------
/public/vendor/toastr/toastr-2.1.3.min.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Note that this is toastr v2.1.3, the "latest" version in url has no more maintenance,
3 | * please go to https://cdnjs.com/libraries/toastr.js and pick a certain version you want to use,
4 | * make sure you copy the url from the website since the url may change between versions.
5 | * */
6 | .toast-title{font-weight:700}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#FFF}.toast-message a:hover{color:#CCC;text-decoration:none}.toast-close-button{position:relative;right:-.3em;top:-.3em;float:right;font-size:20px;font-weight:700;color:#FFF;-webkit-text-shadow:0 1px 0 #fff;text-shadow:0 1px 0 #fff;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80);line-height:1}.toast-close-button:focus,.toast-close-button:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}.rtl .toast-close-button{left:-.3em;float:left;right:.3em}button.toast-close-button{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.toast-top-center{top:0;right:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-left{bottom:12px;left:12px}#toast-container{position:fixed;z-index:999999;pointer-events:none}#toast-container *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#toast-container>div{position:relative;pointer-events:auto;overflow:hidden;margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-position:15px center;background-repeat:no-repeat;-moz-box-shadow:0 0 12px #999;-webkit-box-shadow:0 0 12px #999;box-shadow:0 0 12px #999;color:#FFF;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}#toast-container>div.rtl{direction:rtl;padding:15px 50px 15px 15px;background-position:right 15px center}#toast-container>div:hover{-moz-box-shadow:0 0 12px #000;-webkit-box-shadow:0 0 12px #000;box-shadow:0 0 12px #000;opacity:1;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);filter:alpha(opacity=100);cursor:pointer}#toast-container>.toast-info{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=)!important}#toast-container>.toast-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=)!important}#toast-container>.toast-success{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==)!important}#toast-container>.toast-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=)!important}#toast-container.toast-bottom-center>div,#toast-container.toast-top-center>div{width:300px;margin-left:auto;margin-right:auto}#toast-container.toast-bottom-full-width>div,#toast-container.toast-top-full-width>div{width:96%;margin-left:auto;margin-right:auto}.toast{background-color:#030303}.toast-success{background-color:#51A351}.toast-error{background-color:#BD362F}.toast-info{background-color:#2F96B4}.toast-warning{background-color:#F89406}.toast-progress{position:absolute;left:0;bottom:0;height:4px;background-color:#000;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}@media all and (max-width:240px){#toast-container>div{padding:8px 8px 8px 50px;width:11em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width:241px) and (max-width:480px){#toast-container>div{padding:8px 8px 8px 50px;width:18em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width:481px) and (max-width:768px){#toast-container>div{padding:15px 15px 15px 50px;width:25em}#toast-container>div.rtl{padding:15px 50px 15px 15px}}
7 |
--------------------------------------------------------------------------------
/public/scss/layouts/_navmenu.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Navigation Menu
4 | --------------------------------------------------------------*/
5 | /* Desktop Navigation */
6 | @media (min-width: 1200px) {
7 | .navmenu {
8 | padding: 0;
9 |
10 | ul {
11 | margin: 0;
12 | padding: 0;
13 | display: flex;
14 | list-style: none;
15 | align-items: center;
16 | }
17 |
18 | li {
19 | position: relative;
20 | }
21 |
22 | >ul>li {
23 | white-space: nowrap;
24 | padding: 15px 14px;
25 |
26 | &:last-child {
27 | padding-right: 0;
28 | }
29 | }
30 |
31 | a,
32 | a:focus {
33 | color: var(--nav-color);
34 | font-size: 15px;
35 | padding: 0 2px;
36 | font-family: var(--nav-font);
37 | font-weight: 400;
38 | display: flex;
39 | align-items: center;
40 | justify-content: space-between;
41 | white-space: nowrap;
42 | transition: 0.3s;
43 | position: relative;
44 |
45 | i {
46 | font-size: 12px;
47 | line-height: 0;
48 | margin-left: 5px;
49 | transition: 0.3s;
50 | }
51 | }
52 |
53 | >ul>li>a:before {
54 | content: "";
55 | position: absolute;
56 | height: 2px;
57 | bottom: -6px;
58 | left: 0;
59 | background-color: var(--nav-hover-color);
60 | visibility: hidden;
61 | width: 0px;
62 | transition: all 0.3s ease-in-out 0s;
63 | }
64 |
65 | a:hover:before,
66 | li:hover>a:before,
67 | .active:before {
68 | visibility: visible;
69 | width: 25px;
70 | }
71 |
72 | li:hover>a,
73 | .active,
74 | .active:focus {
75 | color: var(--nav-hover-color);
76 | }
77 |
78 | .dropdown {
79 | ul {
80 | margin: 0;
81 | padding: 10px 0;
82 | background: var(--nav-dropdown-background-color);
83 | display: block;
84 | position: absolute;
85 | visibility: hidden;
86 | left: 14px;
87 | top: 130%;
88 | opacity: 0;
89 | transition: 0.3s;
90 | border-radius: 4px;
91 | z-index: 99;
92 | box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.1);
93 |
94 | li {
95 | min-width: 200px;
96 | }
97 |
98 | a {
99 | padding: 10px 20px;
100 | font-size: 15px;
101 | text-transform: none;
102 | color: var(--nav-dropdown-color);
103 |
104 | i {
105 | font-size: 12px;
106 | }
107 | }
108 |
109 | a:hover,
110 | .active:hover,
111 | li:hover>a {
112 | color: var(--nav-dropdown-hover-color);
113 | }
114 | }
115 |
116 | &:hover>ul {
117 | opacity: 1;
118 | top: 100%;
119 | visibility: visible;
120 | }
121 |
122 | .dropdown {
123 | ul {
124 | top: 0;
125 | left: -90%;
126 | visibility: hidden;
127 | }
128 |
129 | &:hover>ul {
130 | opacity: 1;
131 | top: 0;
132 | left: -100%;
133 | visibility: visible;
134 | }
135 | }
136 | }
137 | }
138 | }
139 |
140 | /* Mobile Navigation */
141 | @media (max-width: 1199px) {
142 | .mobile-nav-toggle {
143 | color: var(--nav-color);
144 | font-size: 28px;
145 | line-height: 0;
146 | margin-right: 10px;
147 | cursor: pointer;
148 | transition: color 0.3s;
149 | }
150 |
151 | .navmenu {
152 | padding: 0;
153 | z-index: 9997;
154 |
155 | ul {
156 | display: none;
157 | list-style: none;
158 | position: absolute;
159 | inset: 60px 20px 20px 20px;
160 | padding: 10px 0;
161 | margin: 0;
162 | border-radius: 6px;
163 | background-color: var(--nav-mobile-background-color);
164 | border: 1px solid color-mix(in srgb, var(--default-color), transparent 90%);
165 | box-shadow: none;
166 | overflow-y: auto;
167 | transition: 0.3s;
168 | z-index: 9998;
169 | }
170 |
171 | a,
172 | a:focus {
173 | color: var(--nav-dropdown-color);
174 | padding: 10px 20px;
175 | font-family: var(--nav-font);
176 | font-size: 17px;
177 | font-weight: 500;
178 | display: flex;
179 | align-items: center;
180 | justify-content: space-between;
181 | white-space: nowrap;
182 | transition: 0.3s;
183 |
184 | i {
185 | font-size: 12px;
186 | line-height: 0;
187 | margin-left: 5px;
188 | width: 30px;
189 | height: 30px;
190 | display: flex;
191 | align-items: center;
192 | justify-content: center;
193 | border-radius: 50%;
194 | transition: 0.3s;
195 | background-color: color-mix(in srgb, var(--accent-color), transparent 90%);
196 |
197 | &:hover {
198 | background-color: var(--accent-color);
199 | color: var(--contrast-color);
200 | }
201 | }
202 | }
203 |
204 | a:hover,
205 | .active,
206 | .active:focus {
207 | color: var(--nav-dropdown-hover-color);
208 | }
209 |
210 | .active i,
211 | .active:focus i {
212 | background-color: var(--accent-color);
213 | color: var(--contrast-color);
214 | transform: rotate(180deg);
215 | }
216 |
217 | .dropdown {
218 | ul {
219 | position: static;
220 | display: none;
221 | z-index: 99;
222 | padding: 10px 0;
223 | margin: 10px 20px;
224 | background-color: var(--nav-dropdown-background-color);
225 | transition: all .5s ease-in-out;
226 |
227 | ul {
228 | background-color: rgba(33, 37, 41, 0.1);
229 | }
230 | }
231 |
232 | >.dropdown-active {
233 | display: block;
234 | background-color: rgba(33, 37, 41, 0.03);
235 | }
236 | }
237 | }
238 |
239 | .mobile-nav-active {
240 | overflow: hidden;
241 |
242 | .mobile-nav-toggle {
243 | color: #fff;
244 | position: absolute;
245 | font-size: 32px;
246 | top: 15px;
247 | right: 15px;
248 | margin-right: 0;
249 | z-index: 9999;
250 | }
251 |
252 | .navmenu {
253 | position: fixed;
254 | overflow: hidden;
255 | inset: 0;
256 | background: rgba(33, 37, 41, 0.8);
257 | transition: 0.3s;
258 |
259 | >ul {
260 | display: block;
261 | }
262 | }
263 | }
264 | }
--------------------------------------------------------------------------------
/store/contact.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags, tagList],
3 | . / header[header],
4 | )
5 |
6 | #' Contact page
7 | #'
8 | #' @export
9 | contact <- \() {
10 | tagList(
11 | header(active = "Contact"),
12 | tags$main(
13 | class = "main",
14 | tags$section(
15 | id = "contact",
16 | class = "contact section",
17 | tags$div(
18 | class = "container section-title",
19 | `data-aos` = "fade-up",
20 | tags$h2("Contact"),
21 | tags$p("Your idea deserves a stage. I’ll help set it.")
22 | ),
23 | tags$div(
24 | class = "container",
25 | `data-aos` = "fade-up",
26 | `data-aos-delay` = "100",
27 | tags$div(
28 | class = "row gy-4",
29 | tags$div(
30 | class = "col-lg-5",
31 | contact_details()
32 | ),
33 | tags$div(
34 | id = "form_container",
35 | class = "col-lg-7",
36 | default_contact_form()
37 | )
38 | ),
39 | tags$span(
40 | id = "loader",
41 | class = "loader htmx-indicator"
42 | )
43 | )
44 | )
45 | )
46 | )
47 | }
48 |
49 | #' Contact details
50 | #'
51 | #' @export
52 | contact_details <- \() {
53 | tags$div(
54 | class = "info-wrap",
55 | info_item(
56 | data_aos_delay = "200",
57 | icon_class = "bi bi-geo-alt",
58 | title = "Address",
59 | description = "Nairobi, Kenya"
60 | ),
61 | info_item(
62 | data_aos_delay = "300",
63 | icon_class = "bi bi-linkedin",
64 | title = "LinkedIn",
65 | description = tags$a(
66 | href = "https://www.linkedin.com/in/kennedymwavu/",
67 | "linkedin.com/in/kennedymwavu"
68 | )
69 | ),
70 | info_item(
71 | data_aos_delay = "400",
72 | icon_class = "bi bi-github",
73 | title = "GitHub",
74 | description = tags$a(
75 | href = "https://github.com/kennedymwavu",
76 | "github.com/kennedymwavu"
77 | )
78 | ),
79 | tags$iframe(
80 | src = "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d127641.17044259232!2d36.76499681772576!3d-1.3030359804811678!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x182f1172d84d49a7%3A0xf7cf0254b297924c!2sNairobi!5e0!3m2!1sen!2ske!4v1725313165835!5m2!1sen!2ske",
81 | frameborder = "0",
82 | style = "border:0; width: 100%; height: 270px",
83 | allowfullscreen = "",
84 | loading = "lazy",
85 | referrerpolicy = "no-referrer-when-downgrade"
86 | )
87 | )
88 | }
89 |
90 | #' Info item
91 | #'
92 | #' @param data_aos_delay String. `data-aos-delay` attribute of the info div.
93 | #' @param icon_class Character vector. Classes to apply to the `` tag.
94 | #' @param title String. Title of the info item.
95 | #' @param description String. Description of the info item.
96 | #' @export
97 | info_item <- \(
98 | data_aos_delay = "100",
99 | icon_class = NULL,
100 | title = NULL,
101 | description = NULL
102 | ) {
103 | icon_class <- paste(icon_class, "flex-shrink-0")
104 |
105 | tags$div(
106 | class = "info-item d-flex",
107 | `data-aos` = "fade-up",
108 | `data-aos-delay` = data_aos_delay,
109 | tags$i(
110 | class = icon_class,
111 | ),
112 | tags$div(
113 | tags$h3(title),
114 | tags$p(description)
115 | )
116 | )
117 | }
118 |
119 | #' Text area input
120 | #'
121 | #' @param label String. [htmltools::tags]. Input label.
122 | #' @param id String. A unique input id.
123 | #' @param value String. Value of the input.
124 | #' @param required Logical. Should the 'required' attribute be added?
125 | #' @param class Character vector. Classes to apply to the input.
126 | #' @param ... [htmltools::tags] to include after the input.
127 | #' @return [htmltools::tags]
128 | #' @export
129 | text_area_input <- \(
130 | ...,
131 | label = NULL,
132 | id = NULL,
133 | value = "",
134 | required = TRUE,
135 | class = NULL
136 | ) {
137 | required <- if (required) NA
138 | class <- c("form-control", class)
139 |
140 | tags$div(
141 | tags$label(
142 | `for` = id,
143 | class = "pb-2",
144 | label
145 | ),
146 | tags$textarea(
147 | class = class,
148 | id = id,
149 | name = id,
150 | rows = "10",
151 | required = required,
152 | value
153 | )
154 | )
155 | }
156 |
157 | #' Text input
158 | #'
159 | #' @param label String. [htmltools::tags]. Input label.
160 | #' @param id String. A unique input id
161 | #' @param type String. Type of the input.
162 | #' Must be one of "text" (default) or "email".
163 | #' @param value String. Value of the input.
164 | #' @param required Logical. Should the 'required' attribute be added?
165 | #' @param class Character vector. Classes to apply to the input.
166 | #' @param ... [htmltools::tags] to include after the input.
167 | #' @return [htmltools::tags]
168 | #' @export
169 | text_input <- \(
170 | ...,
171 | label = NULL,
172 | id = NULL,
173 | type = c("text", "email"),
174 | value = "",
175 | required = TRUE,
176 | class = NULL
177 | ) {
178 | required <- if (required) NA
179 | type <- match.arg(arg = type)
180 | class <- c("form-control", class)
181 |
182 | tags$div(
183 | tags$label(
184 | `for` = id,
185 | class = "pb-2",
186 | label
187 | ),
188 | tags$input(
189 | type = type,
190 | id = id,
191 | name = id,
192 | value = value,
193 | class = class,
194 | required = required
195 | ),
196 | ...
197 | )
198 | }
199 |
200 | #' Contact form
201 | #'
202 | #' @param name [htmltools::tags]
203 | #' @param email [htmltools::tags]
204 | #' @param subject [htmltools::tags]
205 | #' @param message [htmltools::tags]
206 | #' @return [htmltools::tags]
207 | #' @export
208 | contact_form <- \(
209 | name = NULL,
210 | email = NULL,
211 | subject = NULL,
212 | message = NULL
213 | ) {
214 | name <- tags$div(
215 | class = "col-md-6",
216 | name
217 | )
218 |
219 | email <- tags$div(
220 | class = "col-md-6",
221 | email
222 | )
223 |
224 | subject <- tags$div(
225 | class = "col-md-12",
226 | subject
227 | )
228 |
229 | message <- tags$div(
230 | class = "col-md-12",
231 | message
232 | )
233 |
234 | tags$form(
235 | `hx-post` = "/contact",
236 | `hx-target` = "#form_container",
237 | `hx-swap` = "outerHTML",
238 | `hx-indicator` = "#loader",
239 | `data-aos` = "fade-up",
240 | `data-aos-delay` = "200",
241 | class = "php-email-form",
242 | tags$div(
243 | class = "row gy-4",
244 | name,
245 | email,
246 | subject,
247 | message,
248 | tags$div(
249 | class = "col-md-12 text-center",
250 | tags$button(
251 | type = "submit",
252 | "Send Message"
253 | )
254 | )
255 | )
256 | )
257 | }
258 |
259 | #' Default contact form
260 | #'
261 | #' @return [htmltools::tags]
262 | #' @export
263 | default_contact_form <- \() {
264 | contact_form(
265 | name = text_input(
266 | label = "Your Name",
267 | id = "name"
268 | ),
269 | email = text_input(
270 | label = "Your Email",
271 | id = "email",
272 | type = "email"
273 | ),
274 | subject = text_input(
275 | label = "Subject",
276 | id = "subject"
277 | ),
278 | message = text_area_input(
279 | label = "Message",
280 | id = "message"
281 | )
282 | )
283 | }
284 |
--------------------------------------------------------------------------------
/public/scss/sections/_services.scss:
--------------------------------------------------------------------------------
1 | // main: ../main.scss
2 | /*--------------------------------------------------------------
3 | # Services Section
4 | --------------------------------------------------------------*/
5 | .services {
6 | .service-item {
7 | background-color: var(--surface-color);
8 | box-shadow: 0px 5px 90px 0px rgba(0, 0, 0, 0.1);
9 | height: 100%;
10 | padding: 60px 30px;
11 | text-align: center;
12 | transition: 0.3s;
13 | border-radius: 5px;
14 | text-decoration: none;
15 | color: inherit;
16 |
17 | &:focus-visible {
18 | outline: 3px solid color-mix(in srgb, var(--accent-color), transparent 50%);
19 | outline-offset: 6px;
20 | }
21 |
22 | .icon {
23 | margin: 0 auto;
24 | width: 100px;
25 | height: 100px;
26 | display: flex;
27 | align-items: center;
28 | justify-content: center;
29 | transition: ease-in-out 0.3s;
30 | position: relative;
31 |
32 | i {
33 | font-size: 36px;
34 | transition: 0.5s;
35 | position: relative;
36 | }
37 |
38 | svg {
39 | position: absolute;
40 | top: 0;
41 | left: 0;
42 |
43 | path {
44 | transition: 0.5s;
45 | fill: color-mix(in srgb, var(--default-color), transparent 95%);
46 | }
47 | }
48 | }
49 |
50 | h3 {
51 | font-weight: 700;
52 | margin: 10px 0 15px 0;
53 | font-size: 22px;
54 | }
55 |
56 | p {
57 | line-height: 24px;
58 | font-size: 14px;
59 | margin-bottom: 0;
60 | }
61 |
62 | &:hover {
63 | box-shadow: 0px 5px 35px 0px rgba(0, 0, 0, 0.1);
64 | }
65 |
66 | &.item-cyan {
67 | i {
68 | color: #0dcaf0;
69 | }
70 |
71 | &:hover .icon {
72 | i {
73 | color: #fff;
74 | }
75 |
76 | path {
77 | fill: #0dcaf0;
78 | }
79 | }
80 | }
81 |
82 | &.item-orange {
83 | i {
84 | color: #fd7e14;
85 | }
86 |
87 | &:hover .icon {
88 | i {
89 | color: #fff;
90 | }
91 |
92 | path {
93 | fill: #fd7e14;
94 | }
95 | }
96 | }
97 |
98 | &.item-teal {
99 | i {
100 | color: #20c997;
101 | }
102 |
103 | &:hover .icon {
104 | i {
105 | color: #fff;
106 | }
107 |
108 | path {
109 | fill: #20c997;
110 | }
111 | }
112 | }
113 |
114 | &.item-red {
115 | i {
116 | color: #df1529;
117 | }
118 |
119 | &:hover .icon {
120 | i {
121 | color: #fff;
122 | }
123 |
124 | path {
125 | fill: #df1529;
126 | }
127 | }
128 | }
129 |
130 | &.item-indigo {
131 | i {
132 | color: #6610f2;
133 | }
134 |
135 | &:hover .icon {
136 | i {
137 | color: #fff;
138 | }
139 |
140 | path {
141 | fill: #6610f2;
142 | }
143 | }
144 | }
145 |
146 | &.item-pink {
147 | i {
148 | color: #f3268c;
149 | }
150 |
151 | &:hover .icon {
152 | i {
153 | color: #fff;
154 | }
155 |
156 | path {
157 | fill: #f3268c;
158 | }
159 | }
160 | }
161 | }
162 | }
163 |
164 | .service-detail {
165 | .service-detail__back {
166 | display: inline-flex;
167 | align-items: center;
168 | gap: 0.5rem;
169 | font-weight: 600;
170 | letter-spacing: 0.1em;
171 | text-transform: uppercase;
172 |
173 | i {
174 | transition: transform 0.2s ease;
175 | }
176 |
177 | &:hover i,
178 | &:focus i {
179 | transform: translateX(-4px);
180 | }
181 | }
182 |
183 | .service-detail__card {
184 | background: var(--surface-color);
185 | border-radius: 16px;
186 | padding: 2.5rem;
187 | box-shadow: 0px 30px 80px rgba(0, 0, 0, 0.08);
188 | border: 1px solid color-mix(in srgb, var(--default-color) 10%, transparent);
189 | display: grid;
190 | gap: 1.5rem;
191 | transition: box-shadow 0.3s ease;
192 |
193 | &:hover {
194 | box-shadow: 0px 15px 60px rgba(0, 0, 0, 0.12);
195 | }
196 |
197 | &.item-cyan .icon i {
198 | color: #0dcaf0;
199 | }
200 |
201 | &.item-orange .icon i {
202 | color: #fd7e14;
203 | }
204 |
205 | &.item-teal .icon i {
206 | color: #20c997;
207 | }
208 |
209 | &.item-red .icon i {
210 | color: #df1529;
211 | }
212 |
213 | &.item-indigo .icon i {
214 | color: #6610f2;
215 | }
216 |
217 | &.item-pink .icon i {
218 | color: #f3268c;
219 | }
220 |
221 | &.item-cyan:hover .icon,
222 | &.item-cyan:focus-within .icon {
223 | i {
224 | color: #fff;
225 | }
226 |
227 | path {
228 | fill: #0dcaf0;
229 | }
230 | }
231 |
232 | &.item-orange:hover .icon,
233 | &.item-orange:focus-within .icon {
234 | i {
235 | color: #fff;
236 | }
237 |
238 | path {
239 | fill: #fd7e14;
240 | }
241 | }
242 |
243 | &.item-teal:hover .icon,
244 | &.item-teal:focus-within .icon {
245 | i {
246 | color: #fff;
247 | }
248 |
249 | path {
250 | fill: #20c997;
251 | }
252 | }
253 |
254 | &.item-red:hover .icon,
255 | &.item-red:focus-within .icon {
256 | i {
257 | color: #fff;
258 | }
259 |
260 | path {
261 | fill: #df1529;
262 | }
263 | }
264 |
265 | &.item-indigo:hover .icon,
266 | &.item-indigo:focus-within .icon {
267 | i {
268 | color: #fff;
269 | }
270 |
271 | path {
272 | fill: #6610f2;
273 | }
274 | }
275 |
276 | &.item-pink:hover .icon,
277 | &.item-pink:focus-within .icon {
278 | i {
279 | color: #fff;
280 | }
281 |
282 | path {
283 | fill: #f3268c;
284 | }
285 | }
286 | }
287 |
288 | .service-detail__icon {
289 | display: flex;
290 | justify-content: center;
291 |
292 | .icon {
293 | margin: 0 auto;
294 | width: 100px;
295 | height: 100px;
296 | display: flex;
297 | align-items: center;
298 | justify-content: center;
299 | position: relative;
300 |
301 | i {
302 | font-size: 36px;
303 | position: relative;
304 | transition: 0.5s;
305 | }
306 |
307 | svg {
308 | position: absolute;
309 | top: 0;
310 | left: 0;
311 |
312 | path {
313 | fill: color-mix(in srgb, var(--default-color), transparent 95%);
314 | transition: 0.5s;
315 | }
316 | }
317 | }
318 | }
319 |
320 | .service-detail__summary {
321 | font-size: 1.1rem;
322 | color: color-mix(in srgb, var(--default-color), transparent 15%);
323 | }
324 |
325 | .service-detail__list {
326 | background: color-mix(in srgb, var(--accent-color) 6%, transparent);
327 | border-radius: 12px;
328 | padding: 1.5rem;
329 | border: 1px solid color-mix(in srgb, var(--default-color) 8%, transparent);
330 | display: grid;
331 | gap: 1rem;
332 |
333 | h4 {
334 | margin: 0;
335 | text-transform: uppercase;
336 | letter-spacing: 0.08em;
337 | font-size: 0.95rem;
338 | color: var(--heading-color);
339 | }
340 |
341 | ul {
342 | list-style: none;
343 | margin: 0;
344 | padding: 0;
345 | display: grid;
346 | gap: 0.75rem;
347 | }
348 |
349 | li {
350 | display: flex;
351 | gap: 0.75rem;
352 | align-items: flex-start;
353 |
354 | &::before {
355 | content: "•";
356 | color: var(--accent-color);
357 | font-size: 1.25rem;
358 | line-height: 1;
359 | margin-top: 0.1rem;
360 | }
361 | }
362 | }
363 | }
364 |
--------------------------------------------------------------------------------
/public/vendor/bootstrap/css/bootstrap-reboot.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
3 | * Copyright 2011-2024 The Bootstrap Authors
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5 | */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
6 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */
--------------------------------------------------------------------------------
/public/vendor/bootstrap/css/bootstrap-reboot.rtl.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
3 | * Copyright 2011-2024 The Bootstrap Authors
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5 | */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
6 | /*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */
--------------------------------------------------------------------------------
/store/about.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags, tagList],
3 | . / header[header],
4 | . / create_section_title[create_section_title],
5 | )
6 |
7 | #' About page
8 | #'
9 | #' @return [htmltools::tags]
10 | #' @export
11 | about <- \() {
12 | details_on_left <- create_details(
13 | items = c("Blog", "Interests", "Hobbies"),
14 | values = list(
15 | tags$a(
16 | href = "https://blog.mwavu.com/",
17 | target = "_blank",
18 | "blog.mwavu.com"
19 | ),
20 | "Computers & Programming",
21 | "Painting, Philosophy"
22 | )
23 | )
24 | details_on_right <- create_details(
25 | items = c("Degree", "City"),
26 | values = c(
27 | "BSc Actuarial Science",
28 | "Nairobi, Kenya"
29 | )
30 | )
31 | details <- tags$div(
32 | class = "row",
33 | tags$div(
34 | class = "col-lg-6",
35 | details_on_left
36 | ),
37 | tags$div(
38 | class = "col-lg-6",
39 | details_on_right
40 | )
41 | )
42 |
43 | skill_names <- c(
44 | "R",
45 | "REST APIs",
46 | "Docker",
47 | "DigitalOcean",
48 | "Google Cloud",
49 | "Git",
50 | "JavaScript"
51 | )
52 | skill_logos <- paste0(
53 | "assets/img/skills/",
54 | c(
55 | "r.svg",
56 | "openapi.svg",
57 | "docker.svg",
58 | "digitalocean.svg",
59 | "googlecloud.svg",
60 | "git.svg",
61 | "javascript.svg"
62 | )
63 | )
64 |
65 | skills <- tags$div(
66 | class = "row g-4 skills-grid",
67 | create_skills(skill_names, skill_logos)
68 | )
69 |
70 | swiper_config_script <- tags$script(
71 | type = "application/json",
72 | class = "swiper-config",
73 | r'{
74 | {
75 | "loop": true,
76 | "speed": 600,
77 | "autoplay": {
78 | "delay": 5000
79 | },
80 | "slidesPerView": "auto",
81 | "pagination": {
82 | "el": ".swiper-pagination",
83 | "type": "bullets",
84 | "clickable": true
85 | }
86 | }
87 | }'
88 | )
89 | testimonial_items <- create_testimonials(
90 | names = c(
91 | "John Coene",
92 | "Humphreys Wanyanga",
93 | "Grace Zawadi",
94 | "William Wanyonyi"
95 | ),
96 | image_paths = paste0(
97 | "assets/img/testimonials/",
98 | c(
99 | "john.png",
100 | "humphreys.jpeg",
101 | "zawadi.jpeg",
102 | "william.jpeg"
103 | )
104 | ),
105 | job_titles = c(
106 | "Founder, Opifex",
107 | "COO, ACTSERV",
108 | "Project Manager, ACTSERV",
109 | "Business Development Lead, Seven Skies"
110 | ),
111 | statements = c(
112 | "Kennedy’s contributions to the Ambiorix framework have been outstanding. His deep understanding of the framework and innovative input have greatly enhanced its capabilities. His dedication and expertise are evident in every aspect of his work.",
113 | "Kennedy’s technical expertise and commitment to delivering high-quality solutions have been invaluable to our projects. His ability to solve complex problems efficiently makes him an essential part of any team.",
114 | "Working with Kennedy has been a pleasure. He’s reliable, proactive, and always goes the extra mile to ensure project success. His attention to detail and clear communication are exceptional.",
115 | "Kennedy has a unique blend of technical skills and business acumen. His innovative approach and dedication to client needs have greatly contributed to our growth and success."
116 | )
117 | )
118 | testimonials <- tags$div(
119 | class = "swiper init-swiper",
120 | swiper_config_script,
121 | tags$div(
122 | class = "swiper-wrapper",
123 | testimonial_items
124 | ),
125 | tags$div(class = "swiper-pagination")
126 | )
127 |
128 | tagList(
129 | header(active = "About"),
130 | tags$main(
131 | class = "main",
132 | tags$section(
133 | id = "about",
134 | class = "about section",
135 | create_section_title(
136 | title = "About",
137 | subtitle = "I write R for fun & profit."
138 | ),
139 | tags$div(
140 | class = "container",
141 | `data-aos` = "fade-up",
142 | `data-aos-delay` = "100",
143 | tags$div(
144 | class = "row gy-4 justify-content-center",
145 | tags$div(
146 | class = "col-lg-4 d-flex",
147 | tags$img(
148 | src = "assets/img/me.jpeg",
149 | class = "img-fluid object-fit-cover",
150 | alt = "Kennedy Mwavu"
151 | )
152 | ),
153 | tags$div(
154 | class = "col-lg-8 content",
155 | tags$h2("Software Developer"),
156 | tags$p(
157 | class = "fst-italic py-3",
158 | "I started programming in 2020, during the Covid-19 era, while in my second year at the University of Nairobi. A unit on R programming sparked a passion that I've pursued ever since, leading me to shift my focus from Actuarial Science to Software Development — and I've never looked back."
159 | ),
160 | details,
161 | tags$p(
162 | class = "py-3",
163 | "Whether it's automating workflows, developing web applications, or contributing to open-source projects, I enjoy building tools that genuinely improve users' lives."
164 | )
165 | )
166 | )
167 | )
168 | ),
169 | tags$section(
170 | id = "skills",
171 | class = "skills section",
172 | create_section_title(
173 | title = "Skills",
174 | subtitle = "Where my expertise lies"
175 | ),
176 | tags$div(
177 | class = "container",
178 | `data-aos` = "fade-up",
179 | `data-aos-delay` = "100",
180 | skills
181 | )
182 | ),
183 | tags$section(
184 | id = "testimonials",
185 | class = "testimonials section",
186 | create_section_title(
187 | title = "Testimonials",
188 | subtitle = "Hear from those I've had the pleasure of working with"
189 | ),
190 | tags$div(
191 | class = "container",
192 | `data-aos` = "fade-up",
193 | `data-aos-delay` = "100",
194 | testimonials
195 | )
196 | )
197 | )
198 | )
199 | }
200 |
201 | #' Create about page details
202 | #'
203 | #' @param items Character vector, list. Label of the details eg. "Interests".
204 | #' @param values Character vector, list. Contents of `items` eg. "Programming"
205 | #' @return [htmltools::tags]
206 | create_details <- \(items, values) {
207 | list_items <- Map(
208 | f = \(item, value) {
209 | tags$li(
210 | tags$i(class = "bi bi-chevron-right"),
211 | tags$strong(item, ":"),
212 | tags$span(value)
213 | )
214 | },
215 | items,
216 | values
217 | )
218 |
219 | tags$ul(list_items)
220 | }
221 |
222 | #' Create skills
223 | #'
224 | #' @param items Character vector, list. Labels eg. "REST APIs".
225 | #' @param logos Character vector. Paths to logo images.
226 | #' @return [htmltools::tags]
227 | create_skills <- \(items, logos) {
228 | Map(
229 | f = \(item, logo) {
230 | tags$div(
231 | class = "col-12 col-md-4 col-lg-3",
232 | tags$div(
233 | class = "skill-card d-flex align-items-center gap-3",
234 | tags$div(
235 | class = "skill-card__logo d-flex align-items-center justify-content-center",
236 | tags$img(
237 | src = logo,
238 | alt = item,
239 | class = "img-fluid"
240 | )
241 | ),
242 | tags$span(
243 | class = "skill-card__label text-uppercase fw-semibold",
244 | item
245 | )
246 | )
247 | )
248 | },
249 | items,
250 | logos
251 | )
252 | }
253 |
254 | #' Make stats items
255 | #'
256 | #' @param items Character vector, list. Labels eg. "Years of Experience".
257 | #' @param values Character vector, list. Values corresponding to `items`.
258 | #' @return [htmltools::tags]
259 | make_stats <- \(items, values) {
260 | Map(
261 | f = \(item, value) {
262 | tags$div(
263 | class = "col-lg-3 col-md-6",
264 | tags$div(
265 | class = "stats-item text-center w-100 h-100",
266 | tags$span(
267 | `data-purecounter-start` = "0",
268 | `data-purecounter-end` = value,
269 | `data-purecounter-duration` = 1,
270 | class = "purecounter",
271 | ),
272 | tags$p(item)
273 | )
274 | )
275 | },
276 | items,
277 | values
278 | )
279 | }
280 |
281 | #' Create testimonials
282 | #'
283 | #' @param names Character vector. Names of people giving testimonials.
284 | #' @param image_paths Character vector. Paths to the images of those people.
285 | #' @param job_titles Character vector. Their job titles.
286 | #' @param statements Character vector. The testimonials.
287 | #' @return [htmltools::tags]
288 | create_testimonials <- \(names, image_paths, job_titles, statements) {
289 | Map(
290 | f = \(name, image_path, job_title, statement) {
291 | tags$div(
292 | class = "swiper-slide",
293 | tags$div(
294 | class = "testimonial-item",
295 | tags$img(
296 | src = image_path,
297 | class = "testimonial-img",
298 | alt = ""
299 | ),
300 | tags$h3(name),
301 | tags$h4(job_title),
302 | tags$div(
303 | class = "stars",
304 | lapply(1:5, \(i) {
305 | tags$i(class = "bi bi-star-fill")
306 | })
307 | ),
308 | tags$p(
309 | tags$i(class = "bi bi-quote quote-icon-left"),
310 | tags$span(statement),
311 | tags$i(class = "bi bi-quote quote-icon-right")
312 | )
313 | )
314 | )
315 | },
316 | names,
317 | image_paths,
318 | job_titles,
319 | statements
320 | )
321 | }
322 |
--------------------------------------------------------------------------------
/public/vendor/glightbox/css/glightbox.min.css:
--------------------------------------------------------------------------------
1 | .glightbox-container{width:100%;height:100%;position:fixed;top:0;left:0;z-index:999999!important;overflow:hidden;-ms-touch-action:none;touch-action:none;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;outline:0}.glightbox-container.inactive{display:none}.glightbox-container .gcontainer{position:relative;width:100%;height:100%;z-index:9999;overflow:hidden}.glightbox-container .gslider{-webkit-transition:-webkit-transform .4s ease;transition:-webkit-transform .4s ease;transition:transform .4s ease;transition:transform .4s ease,-webkit-transform .4s ease;height:100%;left:0;top:0;width:100%;position:relative;overflow:hidden;display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.glightbox-container .gslide{width:100%;position:absolute;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;opacity:0}.glightbox-container .gslide.current{opacity:1;z-index:99999;position:relative}.glightbox-container .gslide.prev{opacity:1;z-index:9999}.glightbox-container .gslide-inner-content{width:100%}.glightbox-container .ginner-container{position:relative;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;max-width:100%;margin:auto;height:100vh}.glightbox-container .ginner-container.gvideo-container{width:100%}.glightbox-container .ginner-container.desc-bottom,.glightbox-container .ginner-container.desc-top{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.glightbox-container .ginner-container.desc-left,.glightbox-container .ginner-container.desc-right{max-width:100%!important}.gslide iframe,.gslide video{outline:0!important;border:none;min-height:165px;-webkit-overflow-scrolling:touch;-ms-touch-action:auto;touch-action:auto}.gslide:not(.current){pointer-events:none}.gslide-image{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.gslide-image img{max-height:100vh;display:block;padding:0;float:none;outline:0;border:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;max-width:100vw;width:auto;height:auto;-o-object-fit:cover;object-fit:cover;-ms-touch-action:none;touch-action:none;margin:auto;min-width:200px}.desc-bottom .gslide-image img,.desc-top .gslide-image img{width:auto}.desc-left .gslide-image img,.desc-right .gslide-image img{width:auto;max-width:100%}.gslide-image img.zoomable{position:relative}.gslide-image img.dragging{cursor:-webkit-grabbing!important;cursor:grabbing!important;-webkit-transition:none;transition:none}.gslide-video{position:relative;max-width:100vh;width:100%!important}.gslide-video .plyr__poster-enabled.plyr--loading .plyr__poster{display:none}.gslide-video .gvideo-wrapper{width:100%;margin:auto}.gslide-video::before{content:'';position:absolute;width:100%;height:100%;background:rgba(255,0,0,.34);display:none}.gslide-video.playing::before{display:none}.gslide-video.fullscreen{max-width:100%!important;min-width:100%;height:75vh}.gslide-video.fullscreen video{max-width:100%!important;width:100%!important}.gslide-inline{background:#fff;text-align:left;max-height:calc(100vh - 40px);overflow:auto;max-width:100%;margin:auto}.gslide-inline .ginlined-content{padding:20px;width:100%}.gslide-inline .dragging{cursor:-webkit-grabbing!important;cursor:grabbing!important;-webkit-transition:none;transition:none}.ginlined-content{overflow:auto;display:block!important;opacity:1}.gslide-external{display:-webkit-box;display:-ms-flexbox;display:flex;width:100%;min-width:100%;background:#fff;padding:0;overflow:auto;max-height:75vh;height:100%}.gslide-media{display:-webkit-box;display:-ms-flexbox;display:flex;width:auto}.zoomed .gslide-media{-webkit-box-shadow:none!important;box-shadow:none!important}.desc-bottom .gslide-media,.desc-top .gslide-media{margin:0 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.gslide-description{position:relative;-webkit-box-flex:1;-ms-flex:1 0 100%;flex:1 0 100%}.gslide-description.description-left,.gslide-description.description-right{max-width:100%}.gslide-description.description-bottom,.gslide-description.description-top{margin:0 auto;width:100%}.gslide-description p{margin-bottom:12px}.gslide-description p:last-child{margin-bottom:0}.zoomed .gslide-description{display:none}.glightbox-button-hidden{display:none}.glightbox-mobile .glightbox-container .gslide-description{height:auto!important;width:100%;position:absolute;bottom:0;padding:19px 11px;max-width:100vw!important;-webkit-box-ordinal-group:3!important;-ms-flex-order:2!important;order:2!important;max-height:78vh;overflow:auto!important;background:-webkit-gradient(linear,left top,left bottom,from(rgba(0,0,0,0)),to(rgba(0,0,0,.75)));background:linear-gradient(to bottom,rgba(0,0,0,0) 0,rgba(0,0,0,.75) 100%);-webkit-transition:opacity .3s linear;transition:opacity .3s linear;padding-bottom:50px}.glightbox-mobile .glightbox-container .gslide-title{color:#fff;font-size:1em}.glightbox-mobile .glightbox-container .gslide-desc{color:#a1a1a1}.glightbox-mobile .glightbox-container .gslide-desc a{color:#fff;font-weight:700}.glightbox-mobile .glightbox-container .gslide-desc *{color:inherit}.glightbox-mobile .glightbox-container .gslide-desc .desc-more{color:#fff;opacity:.4}.gdesc-open .gslide-media{-webkit-transition:opacity .5s ease;transition:opacity .5s ease;opacity:.4}.gdesc-open .gdesc-inner{padding-bottom:30px}.gdesc-closed .gslide-media{-webkit-transition:opacity .5s ease;transition:opacity .5s ease;opacity:1}.greset{-webkit-transition:all .3s ease;transition:all .3s ease}.gabsolute{position:absolute}.grelative{position:relative}.glightbox-desc{display:none!important}.glightbox-open{overflow:hidden}.gloader{height:25px;width:25px;-webkit-animation:lightboxLoader .8s infinite linear;animation:lightboxLoader .8s infinite linear;border:2px solid #fff;border-right-color:transparent;border-radius:50%;position:absolute;display:block;z-index:9999;left:0;right:0;margin:0 auto;top:47%}.goverlay{width:100%;height:calc(100vh + 1px);position:fixed;top:-1px;left:0;background:#000;will-change:opacity}.glightbox-mobile .goverlay{background:#000}.gclose,.gnext,.gprev{z-index:99999;cursor:pointer;width:26px;height:44px;border:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.gclose svg,.gnext svg,.gprev svg{display:block;width:25px;height:auto;margin:0;padding:0}.gclose.disabled,.gnext.disabled,.gprev.disabled{opacity:.1}.gclose .garrow,.gnext .garrow,.gprev .garrow{stroke:#fff}.gbtn.focused{outline:2px solid #0f3d81}iframe.wait-autoplay{opacity:0}.glightbox-closing .gclose,.glightbox-closing .gnext,.glightbox-closing .gprev{opacity:0!important}.glightbox-clean .gslide-description{background:#fff}.glightbox-clean .gdesc-inner{padding:22px 20px}.glightbox-clean .gslide-title{font-size:1em;font-weight:400;font-family:arial;color:#000;margin-bottom:19px;line-height:1.4em}.glightbox-clean .gslide-desc{font-size:.86em;margin-bottom:0;font-family:arial;line-height:1.4em}.glightbox-clean .gslide-video{background:#000}.glightbox-clean .gclose,.glightbox-clean .gnext,.glightbox-clean .gprev{background-color:rgba(0,0,0,.75);border-radius:4px}.glightbox-clean .gclose path,.glightbox-clean .gnext path,.glightbox-clean .gprev path{fill:#fff}.glightbox-clean .gprev{position:absolute;top:-100%;left:30px;width:40px;height:50px}.glightbox-clean .gnext{position:absolute;top:-100%;right:30px;width:40px;height:50px}.glightbox-clean .gclose{width:35px;height:35px;top:15px;right:10px;position:absolute}.glightbox-clean .gclose svg{width:18px;height:auto}.glightbox-clean .gclose:hover{opacity:1}.gfadeIn{-webkit-animation:gfadeIn .5s ease;animation:gfadeIn .5s ease}.gfadeOut{-webkit-animation:gfadeOut .5s ease;animation:gfadeOut .5s ease}.gslideOutLeft{-webkit-animation:gslideOutLeft .3s ease;animation:gslideOutLeft .3s ease}.gslideInLeft{-webkit-animation:gslideInLeft .3s ease;animation:gslideInLeft .3s ease}.gslideOutRight{-webkit-animation:gslideOutRight .3s ease;animation:gslideOutRight .3s ease}.gslideInRight{-webkit-animation:gslideInRight .3s ease;animation:gslideInRight .3s ease}.gzoomIn{-webkit-animation:gzoomIn .5s ease;animation:gzoomIn .5s ease}.gzoomOut{-webkit-animation:gzoomOut .5s ease;animation:gzoomOut .5s ease}@-webkit-keyframes lightboxLoader{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes lightboxLoader{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes gfadeIn{from{opacity:0}to{opacity:1}}@keyframes gfadeIn{from{opacity:0}to{opacity:1}}@-webkit-keyframes gfadeOut{from{opacity:1}to{opacity:0}}@keyframes gfadeOut{from{opacity:1}to{opacity:0}}@-webkit-keyframes gslideInLeft{from{opacity:0;-webkit-transform:translate3d(-60%,0,0);transform:translate3d(-60%,0,0)}to{visibility:visible;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes gslideInLeft{from{opacity:0;-webkit-transform:translate3d(-60%,0,0);transform:translate3d(-60%,0,0)}to{visibility:visible;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes gslideOutLeft{from{opacity:1;visibility:visible;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}to{-webkit-transform:translate3d(-60%,0,0);transform:translate3d(-60%,0,0);opacity:0;visibility:hidden}}@keyframes gslideOutLeft{from{opacity:1;visibility:visible;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}to{-webkit-transform:translate3d(-60%,0,0);transform:translate3d(-60%,0,0);opacity:0;visibility:hidden}}@-webkit-keyframes gslideInRight{from{opacity:0;visibility:visible;-webkit-transform:translate3d(60%,0,0);transform:translate3d(60%,0,0)}to{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes gslideInRight{from{opacity:0;visibility:visible;-webkit-transform:translate3d(60%,0,0);transform:translate3d(60%,0,0)}to{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes gslideOutRight{from{opacity:1;visibility:visible;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}to{-webkit-transform:translate3d(60%,0,0);transform:translate3d(60%,0,0);opacity:0}}@keyframes gslideOutRight{from{opacity:1;visibility:visible;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}to{-webkit-transform:translate3d(60%,0,0);transform:translate3d(60%,0,0);opacity:0}}@-webkit-keyframes gzoomIn{from{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:1}}@keyframes gzoomIn{from{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:1}}@-webkit-keyframes gzoomOut{from{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:0}}@keyframes gzoomOut{from{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:0}}@media (min-width:769px){.glightbox-container .ginner-container{width:auto;height:auto;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.glightbox-container .ginner-container.desc-top .gslide-description{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.glightbox-container .ginner-container.desc-top .gslide-image,.glightbox-container .ginner-container.desc-top .gslide-image img{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.glightbox-container .ginner-container.desc-left .gslide-description{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.glightbox-container .ginner-container.desc-left .gslide-image{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.gslide-image img{max-height:97vh;max-width:100%}.gslide-image img.zoomable{cursor:-webkit-zoom-in;cursor:zoom-in}.zoomed .gslide-image img.zoomable{cursor:-webkit-grab;cursor:grab}.gslide-inline{max-height:95vh}.gslide-external{max-height:100vh}.gslide-description.description-left,.gslide-description.description-right{max-width:275px}.glightbox-open{height:auto}.goverlay{background:rgba(0,0,0,.92)}.glightbox-clean .gslide-media{-webkit-box-shadow:1px 2px 9px 0 rgba(0,0,0,.65);box-shadow:1px 2px 9px 0 rgba(0,0,0,.65)}.glightbox-clean .description-left .gdesc-inner,.glightbox-clean .description-right .gdesc-inner{position:absolute;height:100%;overflow-y:auto}.glightbox-clean .gclose,.glightbox-clean .gnext,.glightbox-clean .gprev{background-color:rgba(0,0,0,.32)}.glightbox-clean .gclose:hover,.glightbox-clean .gnext:hover,.glightbox-clean .gprev:hover{background-color:rgba(0,0,0,.7)}.glightbox-clean .gprev{top:45%}.glightbox-clean .gnext{top:45%}}@media (min-width:992px){.glightbox-clean .gclose{opacity:.7;right:20px}}@media screen and (max-height:420px){.goverlay{background:#000}}
--------------------------------------------------------------------------------
/public/vendor/aos/aos.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.AOS=t()}(this,function(){"use strict";var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},t="Expected a function",n=NaN,o="[object Symbol]",i=/^\s+|\s+$/g,a=/^[-+]0x[0-9a-f]+$/i,r=/^0b[01]+$/i,c=/^0o[0-7]+$/i,s=parseInt,u="object"==typeof e&&e&&e.Object===Object&&e,d="object"==typeof self&&self&&self.Object===Object&&self,l=u||d||Function("return this")(),f=Object.prototype.toString,m=Math.max,p=Math.min,b=function(){return l.Date.now()};function v(e,n,o){var i,a,r,c,s,u,d=0,l=!1,f=!1,v=!0;if("function"!=typeof e)throw new TypeError(t);function y(t){var n=i,o=a;return i=a=void 0,d=t,c=e.apply(o,n)}function h(e){var t=e-u;return void 0===u||t>=n||t<0||f&&e-d>=r}function k(){var e=b();if(h(e))return x(e);s=setTimeout(k,function(e){var t=n-(e-u);return f?p(t,r-(e-d)):t}(e))}function x(e){return s=void 0,v&&i?y(e):(i=a=void 0,c)}function O(){var e=b(),t=h(e);if(i=arguments,a=this,u=e,t){if(void 0===s)return function(e){return d=e,s=setTimeout(k,n),l?y(e):c}(u);if(f)return s=setTimeout(k,n),y(u)}return void 0===s&&(s=setTimeout(k,n)),c}return n=w(n)||0,g(o)&&(l=!!o.leading,r=(f="maxWait"in o)?m(w(o.maxWait)||0,n):r,v="trailing"in o?!!o.trailing:v),O.cancel=function(){void 0!==s&&clearTimeout(s),d=0,i=u=a=s=void 0},O.flush=function(){return void 0===s?c:x(b())},O}function g(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function w(e){if("number"==typeof e)return e;if(function(e){return"symbol"==typeof e||function(e){return!!e&&"object"==typeof e}(e)&&f.call(e)==o}(e))return n;if(g(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=g(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=e.replace(i,"");var u=r.test(e);return u||c.test(e)?s(e.slice(2),u?2:8):a.test(e)?n:+e}var y=function(e,n,o){var i=!0,a=!0;if("function"!=typeof e)throw new TypeError(t);return g(o)&&(i="leading"in o?!!o.leading:i,a="trailing"in o?!!o.trailing:a),v(e,n,{leading:i,maxWait:n,trailing:a})},h="Expected a function",k=NaN,x="[object Symbol]",O=/^\s+|\s+$/g,j=/^[-+]0x[0-9a-f]+$/i,E=/^0b[01]+$/i,N=/^0o[0-7]+$/i,z=parseInt,C="object"==typeof e&&e&&e.Object===Object&&e,A="object"==typeof self&&self&&self.Object===Object&&self,q=C||A||Function("return this")(),L=Object.prototype.toString,T=Math.max,M=Math.min,S=function(){return q.Date.now()};function D(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function H(e){if("number"==typeof e)return e;if(function(e){return"symbol"==typeof e||function(e){return!!e&&"object"==typeof e}(e)&&L.call(e)==x}(e))return k;if(D(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=D(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=e.replace(O,"");var n=E.test(e);return n||N.test(e)?z(e.slice(2),n?2:8):j.test(e)?k:+e}var $=function(e,t,n){var o,i,a,r,c,s,u=0,d=!1,l=!1,f=!0;if("function"!=typeof e)throw new TypeError(h);function m(t){var n=o,a=i;return o=i=void 0,u=t,r=e.apply(a,n)}function p(e){var n=e-s;return void 0===s||n>=t||n<0||l&&e-u>=a}function b(){var e=S();if(p(e))return v(e);c=setTimeout(b,function(e){var n=t-(e-s);return l?M(n,a-(e-u)):n}(e))}function v(e){return c=void 0,f&&o?m(e):(o=i=void 0,r)}function g(){var e=S(),n=p(e);if(o=arguments,i=this,s=e,n){if(void 0===c)return function(e){return u=e,c=setTimeout(b,t),d?m(e):r}(s);if(l)return c=setTimeout(b,t),m(s)}return void 0===c&&(c=setTimeout(b,t)),r}return t=H(t)||0,D(n)&&(d=!!n.leading,a=(l="maxWait"in n)?T(H(n.maxWait)||0,t):a,f="trailing"in n?!!n.trailing:f),g.cancel=function(){void 0!==c&&clearTimeout(c),u=0,o=s=i=c=void 0},g.flush=function(){return void 0===c?r:v(S())},g},W=function(){};function P(e){e&&e.forEach(function(e){var t=Array.prototype.slice.call(e.addedNodes),n=Array.prototype.slice.call(e.removedNodes);if(function e(t){var n=void 0,o=void 0;for(n=0;n=o.out&&!n.once?a():t>=o.in?e.animated||(function(e,t){t&&t.forEach(function(t){return e.classList.add(t)})}(i,n.animatedClassNames),V("aos:in",i),e.options.id&&V("aos:in:"+e.options.id,i),e.animated=!0):e.animated&&!n.once&&a()}(e,window.pageYOffset)})},Z=function(e){for(var t=0,n=0;e&&!isNaN(e.offsetLeft)&&!isNaN(e.offsetTop);)t+=e.offsetLeft-("BODY"!=e.tagName?e.scrollLeft:0),n+=e.offsetTop-("BODY"!=e.tagName?e.scrollTop:0),e=e.offsetParent;return{top:n,left:t}},ee=function(e,t,n){var o=e.getAttribute("data-aos-"+t);if(void 0!==o){if("true"===o)return!0;if("false"===o)return!1}return o||n},te=function(e,t){return e.forEach(function(e,n){var o=ee(e.node,"mirror",t.mirror),i=ee(e.node,"once",t.once),a=ee(e.node,"id"),r=t.useClassNames&&e.node.getAttribute("data-aos"),c=[t.animatedClassName].concat(r?r.split(" "):[]).filter(function(e){return"string"==typeof e});t.initClassName&&e.node.classList.add(t.initClassName),e.position={in:function(e,t,n){var o=window.innerHeight,i=ee(e,"anchor"),a=ee(e,"anchor-placement"),r=Number(ee(e,"offset",a?0:t)),c=a||n,s=e;i&&document.querySelectorAll(i)&&(s=document.querySelectorAll(i)[0]);var u=Z(s).top-o;switch(c){case"top-bottom":break;case"center-bottom":u+=s.offsetHeight/2;break;case"bottom-bottom":u+=s.offsetHeight;break;case"top-center":u+=o/2;break;case"center-center":u+=o/2+s.offsetHeight/2;break;case"bottom-center":u+=o/2+s.offsetHeight;break;case"top-top":u+=o;break;case"bottom-top":u+=o+s.offsetHeight;break;case"center-top":u+=o+s.offsetHeight/2}return u+r}(e.node,t.offset,t.anchorPlacement),out:o&&function(e,t){window.innerHeight;var n=ee(e,"anchor"),o=ee(e,"offset",t),i=e;return n&&document.querySelectorAll(n)&&(i=document.querySelectorAll(n)[0]),Z(i).top+i.offsetHeight-o}(e.node,t.offset)},e.options={once:i,mirror:o,animatedClassNames:c,id:a}}),e},ne=function(){var e=document.querySelectorAll("[data-aos]");return Array.prototype.map.call(e,function(e){return{node:e}})},oe=[],ie=!1,ae={offset:120,delay:0,easing:"ease",duration:400,disable:!1,once:!1,mirror:!1,anchorPlacement:"top-bottom",startEvent:"DOMContentLoaded",animatedClassName:"aos-animate",initClassName:"aos-init",useClassNames:!1,disableMutationObserver:!1,throttleDelay:99,debounceDelay:50},re=function(){return document.all&&!window.atob},ce=function(){arguments.length>0&&void 0!==arguments[0]&&arguments[0]&&(ie=!0),ie&&(oe=te(oe,ae),X(oe),window.addEventListener("scroll",y(function(){X(oe,ae.once)},ae.throttleDelay)))},se=function(){if(oe=ne(),de(ae.disable)||re())return ue();ce()},ue=function(){oe.forEach(function(e,t){e.node.removeAttribute("data-aos"),e.node.removeAttribute("data-aos-easing"),e.node.removeAttribute("data-aos-duration"),e.node.removeAttribute("data-aos-delay"),ae.initClassName&&e.node.classList.remove(ae.initClassName),ae.animatedClassName&&e.node.classList.remove(ae.animatedClassName)})},de=function(e){return!0===e||"mobile"===e&&U.mobile()||"phone"===e&&U.phone()||"tablet"===e&&U.tablet()||"function"==typeof e&&!0===e()};return{init:function(e){return ae=I(ae,e),oe=ne(),ae.disableMutationObserver||_.isSupported()||(console.info('\n aos: MutationObserver is not supported on this browser,\n code mutations observing has been disabled.\n You may have to call "refreshHard()" by yourself.\n '),ae.disableMutationObserver=!0),ae.disableMutationObserver||_.ready("[data-aos]",se),de(ae.disable)||re()?ue():(document.querySelector("body").setAttribute("data-aos-easing",ae.easing),document.querySelector("body").setAttribute("data-aos-duration",ae.duration),document.querySelector("body").setAttribute("data-aos-delay",ae.delay),-1===["DOMContentLoaded","load"].indexOf(ae.startEvent)?document.addEventListener(ae.startEvent,function(){ce(!0)}):window.addEventListener("load",function(){ce(!0)}),"DOMContentLoaded"===ae.startEvent&&["complete","interactive"].indexOf(document.readyState)>-1&&ce(!0),window.addEventListener("resize",$(ce,ae.debounceDelay,!0)),window.addEventListener("orientationchange",$(ce,ae.debounceDelay,!0)),oe)},refresh:ce,refreshHard:se}});
2 |
--------------------------------------------------------------------------------
/public/vendor/bootstrap/css/bootstrap-reboot.rtl.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
3 | * Copyright 2011-2024 The Bootstrap Authors
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5 | */
6 | :root,
7 | [data-bs-theme=light] {
8 | --bs-blue: #0d6efd;
9 | --bs-indigo: #6610f2;
10 | --bs-purple: #6f42c1;
11 | --bs-pink: #d63384;
12 | --bs-red: #dc3545;
13 | --bs-orange: #fd7e14;
14 | --bs-yellow: #ffc107;
15 | --bs-green: #198754;
16 | --bs-teal: #20c997;
17 | --bs-cyan: #0dcaf0;
18 | --bs-black: #000;
19 | --bs-white: #fff;
20 | --bs-gray: #6c757d;
21 | --bs-gray-dark: #343a40;
22 | --bs-gray-100: #f8f9fa;
23 | --bs-gray-200: #e9ecef;
24 | --bs-gray-300: #dee2e6;
25 | --bs-gray-400: #ced4da;
26 | --bs-gray-500: #adb5bd;
27 | --bs-gray-600: #6c757d;
28 | --bs-gray-700: #495057;
29 | --bs-gray-800: #343a40;
30 | --bs-gray-900: #212529;
31 | --bs-primary: #0d6efd;
32 | --bs-secondary: #6c757d;
33 | --bs-success: #198754;
34 | --bs-info: #0dcaf0;
35 | --bs-warning: #ffc107;
36 | --bs-danger: #dc3545;
37 | --bs-light: #f8f9fa;
38 | --bs-dark: #212529;
39 | --bs-primary-rgb: 13, 110, 253;
40 | --bs-secondary-rgb: 108, 117, 125;
41 | --bs-success-rgb: 25, 135, 84;
42 | --bs-info-rgb: 13, 202, 240;
43 | --bs-warning-rgb: 255, 193, 7;
44 | --bs-danger-rgb: 220, 53, 69;
45 | --bs-light-rgb: 248, 249, 250;
46 | --bs-dark-rgb: 33, 37, 41;
47 | --bs-primary-text-emphasis: #052c65;
48 | --bs-secondary-text-emphasis: #2b2f32;
49 | --bs-success-text-emphasis: #0a3622;
50 | --bs-info-text-emphasis: #055160;
51 | --bs-warning-text-emphasis: #664d03;
52 | --bs-danger-text-emphasis: #58151c;
53 | --bs-light-text-emphasis: #495057;
54 | --bs-dark-text-emphasis: #495057;
55 | --bs-primary-bg-subtle: #cfe2ff;
56 | --bs-secondary-bg-subtle: #e2e3e5;
57 | --bs-success-bg-subtle: #d1e7dd;
58 | --bs-info-bg-subtle: #cff4fc;
59 | --bs-warning-bg-subtle: #fff3cd;
60 | --bs-danger-bg-subtle: #f8d7da;
61 | --bs-light-bg-subtle: #fcfcfd;
62 | --bs-dark-bg-subtle: #ced4da;
63 | --bs-primary-border-subtle: #9ec5fe;
64 | --bs-secondary-border-subtle: #c4c8cb;
65 | --bs-success-border-subtle: #a3cfbb;
66 | --bs-info-border-subtle: #9eeaf9;
67 | --bs-warning-border-subtle: #ffe69c;
68 | --bs-danger-border-subtle: #f1aeb5;
69 | --bs-light-border-subtle: #e9ecef;
70 | --bs-dark-border-subtle: #adb5bd;
71 | --bs-white-rgb: 255, 255, 255;
72 | --bs-black-rgb: 0, 0, 0;
73 | --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
74 | --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
75 | --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
76 | --bs-body-font-family: var(--bs-font-sans-serif);
77 | --bs-body-font-size: 1rem;
78 | --bs-body-font-weight: 400;
79 | --bs-body-line-height: 1.5;
80 | --bs-body-color: #212529;
81 | --bs-body-color-rgb: 33, 37, 41;
82 | --bs-body-bg: #fff;
83 | --bs-body-bg-rgb: 255, 255, 255;
84 | --bs-emphasis-color: #000;
85 | --bs-emphasis-color-rgb: 0, 0, 0;
86 | --bs-secondary-color: rgba(33, 37, 41, 0.75);
87 | --bs-secondary-color-rgb: 33, 37, 41;
88 | --bs-secondary-bg: #e9ecef;
89 | --bs-secondary-bg-rgb: 233, 236, 239;
90 | --bs-tertiary-color: rgba(33, 37, 41, 0.5);
91 | --bs-tertiary-color-rgb: 33, 37, 41;
92 | --bs-tertiary-bg: #f8f9fa;
93 | --bs-tertiary-bg-rgb: 248, 249, 250;
94 | --bs-heading-color: inherit;
95 | --bs-link-color: #0d6efd;
96 | --bs-link-color-rgb: 13, 110, 253;
97 | --bs-link-decoration: underline;
98 | --bs-link-hover-color: #0a58ca;
99 | --bs-link-hover-color-rgb: 10, 88, 202;
100 | --bs-code-color: #d63384;
101 | --bs-highlight-color: #212529;
102 | --bs-highlight-bg: #fff3cd;
103 | --bs-border-width: 1px;
104 | --bs-border-style: solid;
105 | --bs-border-color: #dee2e6;
106 | --bs-border-color-translucent: rgba(0, 0, 0, 0.175);
107 | --bs-border-radius: 0.375rem;
108 | --bs-border-radius-sm: 0.25rem;
109 | --bs-border-radius-lg: 0.5rem;
110 | --bs-border-radius-xl: 1rem;
111 | --bs-border-radius-xxl: 2rem;
112 | --bs-border-radius-2xl: var(--bs-border-radius-xxl);
113 | --bs-border-radius-pill: 50rem;
114 | --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
115 | --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
116 | --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
117 | --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
118 | --bs-focus-ring-width: 0.25rem;
119 | --bs-focus-ring-opacity: 0.25;
120 | --bs-focus-ring-color: rgba(13, 110, 253, 0.25);
121 | --bs-form-valid-color: #198754;
122 | --bs-form-valid-border-color: #198754;
123 | --bs-form-invalid-color: #dc3545;
124 | --bs-form-invalid-border-color: #dc3545;
125 | }
126 |
127 | [data-bs-theme=dark] {
128 | color-scheme: dark;
129 | --bs-body-color: #dee2e6;
130 | --bs-body-color-rgb: 222, 226, 230;
131 | --bs-body-bg: #212529;
132 | --bs-body-bg-rgb: 33, 37, 41;
133 | --bs-emphasis-color: #fff;
134 | --bs-emphasis-color-rgb: 255, 255, 255;
135 | --bs-secondary-color: rgba(222, 226, 230, 0.75);
136 | --bs-secondary-color-rgb: 222, 226, 230;
137 | --bs-secondary-bg: #343a40;
138 | --bs-secondary-bg-rgb: 52, 58, 64;
139 | --bs-tertiary-color: rgba(222, 226, 230, 0.5);
140 | --bs-tertiary-color-rgb: 222, 226, 230;
141 | --bs-tertiary-bg: #2b3035;
142 | --bs-tertiary-bg-rgb: 43, 48, 53;
143 | --bs-primary-text-emphasis: #6ea8fe;
144 | --bs-secondary-text-emphasis: #a7acb1;
145 | --bs-success-text-emphasis: #75b798;
146 | --bs-info-text-emphasis: #6edff6;
147 | --bs-warning-text-emphasis: #ffda6a;
148 | --bs-danger-text-emphasis: #ea868f;
149 | --bs-light-text-emphasis: #f8f9fa;
150 | --bs-dark-text-emphasis: #dee2e6;
151 | --bs-primary-bg-subtle: #031633;
152 | --bs-secondary-bg-subtle: #161719;
153 | --bs-success-bg-subtle: #051b11;
154 | --bs-info-bg-subtle: #032830;
155 | --bs-warning-bg-subtle: #332701;
156 | --bs-danger-bg-subtle: #2c0b0e;
157 | --bs-light-bg-subtle: #343a40;
158 | --bs-dark-bg-subtle: #1a1d20;
159 | --bs-primary-border-subtle: #084298;
160 | --bs-secondary-border-subtle: #41464b;
161 | --bs-success-border-subtle: #0f5132;
162 | --bs-info-border-subtle: #087990;
163 | --bs-warning-border-subtle: #997404;
164 | --bs-danger-border-subtle: #842029;
165 | --bs-light-border-subtle: #495057;
166 | --bs-dark-border-subtle: #343a40;
167 | --bs-heading-color: inherit;
168 | --bs-link-color: #6ea8fe;
169 | --bs-link-hover-color: #8bb9fe;
170 | --bs-link-color-rgb: 110, 168, 254;
171 | --bs-link-hover-color-rgb: 139, 185, 254;
172 | --bs-code-color: #e685b5;
173 | --bs-highlight-color: #dee2e6;
174 | --bs-highlight-bg: #664d03;
175 | --bs-border-color: #495057;
176 | --bs-border-color-translucent: rgba(255, 255, 255, 0.15);
177 | --bs-form-valid-color: #75b798;
178 | --bs-form-valid-border-color: #75b798;
179 | --bs-form-invalid-color: #ea868f;
180 | --bs-form-invalid-border-color: #ea868f;
181 | }
182 |
183 | *,
184 | *::before,
185 | *::after {
186 | box-sizing: border-box;
187 | }
188 |
189 | @media (prefers-reduced-motion: no-preference) {
190 | :root {
191 | scroll-behavior: smooth;
192 | }
193 | }
194 |
195 | body {
196 | margin: 0;
197 | font-family: var(--bs-body-font-family);
198 | font-size: var(--bs-body-font-size);
199 | font-weight: var(--bs-body-font-weight);
200 | line-height: var(--bs-body-line-height);
201 | color: var(--bs-body-color);
202 | text-align: var(--bs-body-text-align);
203 | background-color: var(--bs-body-bg);
204 | -webkit-text-size-adjust: 100%;
205 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
206 | }
207 |
208 | hr {
209 | margin: 1rem 0;
210 | color: inherit;
211 | border: 0;
212 | border-top: var(--bs-border-width) solid;
213 | opacity: 0.25;
214 | }
215 |
216 | h6, h5, h4, h3, h2, h1 {
217 | margin-top: 0;
218 | margin-bottom: 0.5rem;
219 | font-weight: 500;
220 | line-height: 1.2;
221 | color: var(--bs-heading-color);
222 | }
223 |
224 | h1 {
225 | font-size: calc(1.375rem + 1.5vw);
226 | }
227 | @media (min-width: 1200px) {
228 | h1 {
229 | font-size: 2.5rem;
230 | }
231 | }
232 |
233 | h2 {
234 | font-size: calc(1.325rem + 0.9vw);
235 | }
236 | @media (min-width: 1200px) {
237 | h2 {
238 | font-size: 2rem;
239 | }
240 | }
241 |
242 | h3 {
243 | font-size: calc(1.3rem + 0.6vw);
244 | }
245 | @media (min-width: 1200px) {
246 | h3 {
247 | font-size: 1.75rem;
248 | }
249 | }
250 |
251 | h4 {
252 | font-size: calc(1.275rem + 0.3vw);
253 | }
254 | @media (min-width: 1200px) {
255 | h4 {
256 | font-size: 1.5rem;
257 | }
258 | }
259 |
260 | h5 {
261 | font-size: 1.25rem;
262 | }
263 |
264 | h6 {
265 | font-size: 1rem;
266 | }
267 |
268 | p {
269 | margin-top: 0;
270 | margin-bottom: 1rem;
271 | }
272 |
273 | abbr[title] {
274 | -webkit-text-decoration: underline dotted;
275 | text-decoration: underline dotted;
276 | cursor: help;
277 | -webkit-text-decoration-skip-ink: none;
278 | text-decoration-skip-ink: none;
279 | }
280 |
281 | address {
282 | margin-bottom: 1rem;
283 | font-style: normal;
284 | line-height: inherit;
285 | }
286 |
287 | ol,
288 | ul {
289 | padding-right: 2rem;
290 | }
291 |
292 | ol,
293 | ul,
294 | dl {
295 | margin-top: 0;
296 | margin-bottom: 1rem;
297 | }
298 |
299 | ol ol,
300 | ul ul,
301 | ol ul,
302 | ul ol {
303 | margin-bottom: 0;
304 | }
305 |
306 | dt {
307 | font-weight: 700;
308 | }
309 |
310 | dd {
311 | margin-bottom: 0.5rem;
312 | margin-right: 0;
313 | }
314 |
315 | blockquote {
316 | margin: 0 0 1rem;
317 | }
318 |
319 | b,
320 | strong {
321 | font-weight: bolder;
322 | }
323 |
324 | small {
325 | font-size: 0.875em;
326 | }
327 |
328 | mark {
329 | padding: 0.1875em;
330 | color: var(--bs-highlight-color);
331 | background-color: var(--bs-highlight-bg);
332 | }
333 |
334 | sub,
335 | sup {
336 | position: relative;
337 | font-size: 0.75em;
338 | line-height: 0;
339 | vertical-align: baseline;
340 | }
341 |
342 | sub {
343 | bottom: -0.25em;
344 | }
345 |
346 | sup {
347 | top: -0.5em;
348 | }
349 |
350 | a {
351 | color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
352 | text-decoration: underline;
353 | }
354 | a:hover {
355 | --bs-link-color-rgb: var(--bs-link-hover-color-rgb);
356 | }
357 |
358 | a:not([href]):not([class]), a:not([href]):not([class]):hover {
359 | color: inherit;
360 | text-decoration: none;
361 | }
362 |
363 | pre,
364 | code,
365 | kbd,
366 | samp {
367 | font-family: var(--bs-font-monospace);
368 | font-size: 1em;
369 | }
370 |
371 | pre {
372 | display: block;
373 | margin-top: 0;
374 | margin-bottom: 1rem;
375 | overflow: auto;
376 | font-size: 0.875em;
377 | }
378 | pre code {
379 | font-size: inherit;
380 | color: inherit;
381 | word-break: normal;
382 | }
383 |
384 | code {
385 | font-size: 0.875em;
386 | color: var(--bs-code-color);
387 | word-wrap: break-word;
388 | }
389 | a > code {
390 | color: inherit;
391 | }
392 |
393 | kbd {
394 | padding: 0.1875rem 0.375rem;
395 | font-size: 0.875em;
396 | color: var(--bs-body-bg);
397 | background-color: var(--bs-body-color);
398 | border-radius: 0.25rem;
399 | }
400 | kbd kbd {
401 | padding: 0;
402 | font-size: 1em;
403 | }
404 |
405 | figure {
406 | margin: 0 0 1rem;
407 | }
408 |
409 | img,
410 | svg {
411 | vertical-align: middle;
412 | }
413 |
414 | table {
415 | caption-side: bottom;
416 | border-collapse: collapse;
417 | }
418 |
419 | caption {
420 | padding-top: 0.5rem;
421 | padding-bottom: 0.5rem;
422 | color: var(--bs-secondary-color);
423 | text-align: right;
424 | }
425 |
426 | th {
427 | text-align: inherit;
428 | text-align: -webkit-match-parent;
429 | }
430 |
431 | thead,
432 | tbody,
433 | tfoot,
434 | tr,
435 | td,
436 | th {
437 | border-color: inherit;
438 | border-style: solid;
439 | border-width: 0;
440 | }
441 |
442 | label {
443 | display: inline-block;
444 | }
445 |
446 | button {
447 | border-radius: 0;
448 | }
449 |
450 | button:focus:not(:focus-visible) {
451 | outline: 0;
452 | }
453 |
454 | input,
455 | button,
456 | select,
457 | optgroup,
458 | textarea {
459 | margin: 0;
460 | font-family: inherit;
461 | font-size: inherit;
462 | line-height: inherit;
463 | }
464 |
465 | button,
466 | select {
467 | text-transform: none;
468 | }
469 |
470 | [role=button] {
471 | cursor: pointer;
472 | }
473 |
474 | select {
475 | word-wrap: normal;
476 | }
477 | select:disabled {
478 | opacity: 1;
479 | }
480 |
481 | [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
482 | display: none !important;
483 | }
484 |
485 | button,
486 | [type=button],
487 | [type=reset],
488 | [type=submit] {
489 | -webkit-appearance: button;
490 | }
491 | button:not(:disabled),
492 | [type=button]:not(:disabled),
493 | [type=reset]:not(:disabled),
494 | [type=submit]:not(:disabled) {
495 | cursor: pointer;
496 | }
497 |
498 | ::-moz-focus-inner {
499 | padding: 0;
500 | border-style: none;
501 | }
502 |
503 | textarea {
504 | resize: vertical;
505 | }
506 |
507 | fieldset {
508 | min-width: 0;
509 | padding: 0;
510 | margin: 0;
511 | border: 0;
512 | }
513 |
514 | legend {
515 | float: right;
516 | width: 100%;
517 | padding: 0;
518 | margin-bottom: 0.5rem;
519 | font-size: calc(1.275rem + 0.3vw);
520 | line-height: inherit;
521 | }
522 | @media (min-width: 1200px) {
523 | legend {
524 | font-size: 1.5rem;
525 | }
526 | }
527 | legend + * {
528 | clear: right;
529 | }
530 |
531 | ::-webkit-datetime-edit-fields-wrapper,
532 | ::-webkit-datetime-edit-text,
533 | ::-webkit-datetime-edit-minute,
534 | ::-webkit-datetime-edit-hour-field,
535 | ::-webkit-datetime-edit-day-field,
536 | ::-webkit-datetime-edit-month-field,
537 | ::-webkit-datetime-edit-year-field {
538 | padding: 0;
539 | }
540 |
541 | ::-webkit-inner-spin-button {
542 | height: auto;
543 | }
544 |
545 | [type=search] {
546 | -webkit-appearance: textfield;
547 | outline-offset: -2px;
548 | }
549 |
550 | [type="tel"],
551 | [type="url"],
552 | [type="email"],
553 | [type="number"] {
554 | direction: ltr;
555 | }
556 | ::-webkit-search-decoration {
557 | -webkit-appearance: none;
558 | }
559 |
560 | ::-webkit-color-swatch-wrapper {
561 | padding: 0;
562 | }
563 |
564 | ::-webkit-file-upload-button {
565 | font: inherit;
566 | -webkit-appearance: button;
567 | }
568 |
569 | ::file-selector-button {
570 | font: inherit;
571 | -webkit-appearance: button;
572 | }
573 |
574 | output {
575 | display: inline-block;
576 | }
577 |
578 | iframe {
579 | border: 0;
580 | }
581 |
582 | summary {
583 | display: list-item;
584 | cursor: pointer;
585 | }
586 |
587 | progress {
588 | vertical-align: baseline;
589 | }
590 |
591 | [hidden] {
592 | display: none !important;
593 | }
594 | /*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
--------------------------------------------------------------------------------
/public/vendor/bootstrap/css/bootstrap-reboot.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
3 | * Copyright 2011-2024 The Bootstrap Authors
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5 | */
6 | :root,
7 | [data-bs-theme=light] {
8 | --bs-blue: #0d6efd;
9 | --bs-indigo: #6610f2;
10 | --bs-purple: #6f42c1;
11 | --bs-pink: #d63384;
12 | --bs-red: #dc3545;
13 | --bs-orange: #fd7e14;
14 | --bs-yellow: #ffc107;
15 | --bs-green: #198754;
16 | --bs-teal: #20c997;
17 | --bs-cyan: #0dcaf0;
18 | --bs-black: #000;
19 | --bs-white: #fff;
20 | --bs-gray: #6c757d;
21 | --bs-gray-dark: #343a40;
22 | --bs-gray-100: #f8f9fa;
23 | --bs-gray-200: #e9ecef;
24 | --bs-gray-300: #dee2e6;
25 | --bs-gray-400: #ced4da;
26 | --bs-gray-500: #adb5bd;
27 | --bs-gray-600: #6c757d;
28 | --bs-gray-700: #495057;
29 | --bs-gray-800: #343a40;
30 | --bs-gray-900: #212529;
31 | --bs-primary: #0d6efd;
32 | --bs-secondary: #6c757d;
33 | --bs-success: #198754;
34 | --bs-info: #0dcaf0;
35 | --bs-warning: #ffc107;
36 | --bs-danger: #dc3545;
37 | --bs-light: #f8f9fa;
38 | --bs-dark: #212529;
39 | --bs-primary-rgb: 13, 110, 253;
40 | --bs-secondary-rgb: 108, 117, 125;
41 | --bs-success-rgb: 25, 135, 84;
42 | --bs-info-rgb: 13, 202, 240;
43 | --bs-warning-rgb: 255, 193, 7;
44 | --bs-danger-rgb: 220, 53, 69;
45 | --bs-light-rgb: 248, 249, 250;
46 | --bs-dark-rgb: 33, 37, 41;
47 | --bs-primary-text-emphasis: #052c65;
48 | --bs-secondary-text-emphasis: #2b2f32;
49 | --bs-success-text-emphasis: #0a3622;
50 | --bs-info-text-emphasis: #055160;
51 | --bs-warning-text-emphasis: #664d03;
52 | --bs-danger-text-emphasis: #58151c;
53 | --bs-light-text-emphasis: #495057;
54 | --bs-dark-text-emphasis: #495057;
55 | --bs-primary-bg-subtle: #cfe2ff;
56 | --bs-secondary-bg-subtle: #e2e3e5;
57 | --bs-success-bg-subtle: #d1e7dd;
58 | --bs-info-bg-subtle: #cff4fc;
59 | --bs-warning-bg-subtle: #fff3cd;
60 | --bs-danger-bg-subtle: #f8d7da;
61 | --bs-light-bg-subtle: #fcfcfd;
62 | --bs-dark-bg-subtle: #ced4da;
63 | --bs-primary-border-subtle: #9ec5fe;
64 | --bs-secondary-border-subtle: #c4c8cb;
65 | --bs-success-border-subtle: #a3cfbb;
66 | --bs-info-border-subtle: #9eeaf9;
67 | --bs-warning-border-subtle: #ffe69c;
68 | --bs-danger-border-subtle: #f1aeb5;
69 | --bs-light-border-subtle: #e9ecef;
70 | --bs-dark-border-subtle: #adb5bd;
71 | --bs-white-rgb: 255, 255, 255;
72 | --bs-black-rgb: 0, 0, 0;
73 | --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
74 | --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
75 | --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
76 | --bs-body-font-family: var(--bs-font-sans-serif);
77 | --bs-body-font-size: 1rem;
78 | --bs-body-font-weight: 400;
79 | --bs-body-line-height: 1.5;
80 | --bs-body-color: #212529;
81 | --bs-body-color-rgb: 33, 37, 41;
82 | --bs-body-bg: #fff;
83 | --bs-body-bg-rgb: 255, 255, 255;
84 | --bs-emphasis-color: #000;
85 | --bs-emphasis-color-rgb: 0, 0, 0;
86 | --bs-secondary-color: rgba(33, 37, 41, 0.75);
87 | --bs-secondary-color-rgb: 33, 37, 41;
88 | --bs-secondary-bg: #e9ecef;
89 | --bs-secondary-bg-rgb: 233, 236, 239;
90 | --bs-tertiary-color: rgba(33, 37, 41, 0.5);
91 | --bs-tertiary-color-rgb: 33, 37, 41;
92 | --bs-tertiary-bg: #f8f9fa;
93 | --bs-tertiary-bg-rgb: 248, 249, 250;
94 | --bs-heading-color: inherit;
95 | --bs-link-color: #0d6efd;
96 | --bs-link-color-rgb: 13, 110, 253;
97 | --bs-link-decoration: underline;
98 | --bs-link-hover-color: #0a58ca;
99 | --bs-link-hover-color-rgb: 10, 88, 202;
100 | --bs-code-color: #d63384;
101 | --bs-highlight-color: #212529;
102 | --bs-highlight-bg: #fff3cd;
103 | --bs-border-width: 1px;
104 | --bs-border-style: solid;
105 | --bs-border-color: #dee2e6;
106 | --bs-border-color-translucent: rgba(0, 0, 0, 0.175);
107 | --bs-border-radius: 0.375rem;
108 | --bs-border-radius-sm: 0.25rem;
109 | --bs-border-radius-lg: 0.5rem;
110 | --bs-border-radius-xl: 1rem;
111 | --bs-border-radius-xxl: 2rem;
112 | --bs-border-radius-2xl: var(--bs-border-radius-xxl);
113 | --bs-border-radius-pill: 50rem;
114 | --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
115 | --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
116 | --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
117 | --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
118 | --bs-focus-ring-width: 0.25rem;
119 | --bs-focus-ring-opacity: 0.25;
120 | --bs-focus-ring-color: rgba(13, 110, 253, 0.25);
121 | --bs-form-valid-color: #198754;
122 | --bs-form-valid-border-color: #198754;
123 | --bs-form-invalid-color: #dc3545;
124 | --bs-form-invalid-border-color: #dc3545;
125 | }
126 |
127 | [data-bs-theme=dark] {
128 | color-scheme: dark;
129 | --bs-body-color: #dee2e6;
130 | --bs-body-color-rgb: 222, 226, 230;
131 | --bs-body-bg: #212529;
132 | --bs-body-bg-rgb: 33, 37, 41;
133 | --bs-emphasis-color: #fff;
134 | --bs-emphasis-color-rgb: 255, 255, 255;
135 | --bs-secondary-color: rgba(222, 226, 230, 0.75);
136 | --bs-secondary-color-rgb: 222, 226, 230;
137 | --bs-secondary-bg: #343a40;
138 | --bs-secondary-bg-rgb: 52, 58, 64;
139 | --bs-tertiary-color: rgba(222, 226, 230, 0.5);
140 | --bs-tertiary-color-rgb: 222, 226, 230;
141 | --bs-tertiary-bg: #2b3035;
142 | --bs-tertiary-bg-rgb: 43, 48, 53;
143 | --bs-primary-text-emphasis: #6ea8fe;
144 | --bs-secondary-text-emphasis: #a7acb1;
145 | --bs-success-text-emphasis: #75b798;
146 | --bs-info-text-emphasis: #6edff6;
147 | --bs-warning-text-emphasis: #ffda6a;
148 | --bs-danger-text-emphasis: #ea868f;
149 | --bs-light-text-emphasis: #f8f9fa;
150 | --bs-dark-text-emphasis: #dee2e6;
151 | --bs-primary-bg-subtle: #031633;
152 | --bs-secondary-bg-subtle: #161719;
153 | --bs-success-bg-subtle: #051b11;
154 | --bs-info-bg-subtle: #032830;
155 | --bs-warning-bg-subtle: #332701;
156 | --bs-danger-bg-subtle: #2c0b0e;
157 | --bs-light-bg-subtle: #343a40;
158 | --bs-dark-bg-subtle: #1a1d20;
159 | --bs-primary-border-subtle: #084298;
160 | --bs-secondary-border-subtle: #41464b;
161 | --bs-success-border-subtle: #0f5132;
162 | --bs-info-border-subtle: #087990;
163 | --bs-warning-border-subtle: #997404;
164 | --bs-danger-border-subtle: #842029;
165 | --bs-light-border-subtle: #495057;
166 | --bs-dark-border-subtle: #343a40;
167 | --bs-heading-color: inherit;
168 | --bs-link-color: #6ea8fe;
169 | --bs-link-hover-color: #8bb9fe;
170 | --bs-link-color-rgb: 110, 168, 254;
171 | --bs-link-hover-color-rgb: 139, 185, 254;
172 | --bs-code-color: #e685b5;
173 | --bs-highlight-color: #dee2e6;
174 | --bs-highlight-bg: #664d03;
175 | --bs-border-color: #495057;
176 | --bs-border-color-translucent: rgba(255, 255, 255, 0.15);
177 | --bs-form-valid-color: #75b798;
178 | --bs-form-valid-border-color: #75b798;
179 | --bs-form-invalid-color: #ea868f;
180 | --bs-form-invalid-border-color: #ea868f;
181 | }
182 |
183 | *,
184 | *::before,
185 | *::after {
186 | box-sizing: border-box;
187 | }
188 |
189 | @media (prefers-reduced-motion: no-preference) {
190 | :root {
191 | scroll-behavior: smooth;
192 | }
193 | }
194 |
195 | body {
196 | margin: 0;
197 | font-family: var(--bs-body-font-family);
198 | font-size: var(--bs-body-font-size);
199 | font-weight: var(--bs-body-font-weight);
200 | line-height: var(--bs-body-line-height);
201 | color: var(--bs-body-color);
202 | text-align: var(--bs-body-text-align);
203 | background-color: var(--bs-body-bg);
204 | -webkit-text-size-adjust: 100%;
205 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
206 | }
207 |
208 | hr {
209 | margin: 1rem 0;
210 | color: inherit;
211 | border: 0;
212 | border-top: var(--bs-border-width) solid;
213 | opacity: 0.25;
214 | }
215 |
216 | h6, h5, h4, h3, h2, h1 {
217 | margin-top: 0;
218 | margin-bottom: 0.5rem;
219 | font-weight: 500;
220 | line-height: 1.2;
221 | color: var(--bs-heading-color);
222 | }
223 |
224 | h1 {
225 | font-size: calc(1.375rem + 1.5vw);
226 | }
227 | @media (min-width: 1200px) {
228 | h1 {
229 | font-size: 2.5rem;
230 | }
231 | }
232 |
233 | h2 {
234 | font-size: calc(1.325rem + 0.9vw);
235 | }
236 | @media (min-width: 1200px) {
237 | h2 {
238 | font-size: 2rem;
239 | }
240 | }
241 |
242 | h3 {
243 | font-size: calc(1.3rem + 0.6vw);
244 | }
245 | @media (min-width: 1200px) {
246 | h3 {
247 | font-size: 1.75rem;
248 | }
249 | }
250 |
251 | h4 {
252 | font-size: calc(1.275rem + 0.3vw);
253 | }
254 | @media (min-width: 1200px) {
255 | h4 {
256 | font-size: 1.5rem;
257 | }
258 | }
259 |
260 | h5 {
261 | font-size: 1.25rem;
262 | }
263 |
264 | h6 {
265 | font-size: 1rem;
266 | }
267 |
268 | p {
269 | margin-top: 0;
270 | margin-bottom: 1rem;
271 | }
272 |
273 | abbr[title] {
274 | -webkit-text-decoration: underline dotted;
275 | text-decoration: underline dotted;
276 | cursor: help;
277 | -webkit-text-decoration-skip-ink: none;
278 | text-decoration-skip-ink: none;
279 | }
280 |
281 | address {
282 | margin-bottom: 1rem;
283 | font-style: normal;
284 | line-height: inherit;
285 | }
286 |
287 | ol,
288 | ul {
289 | padding-left: 2rem;
290 | }
291 |
292 | ol,
293 | ul,
294 | dl {
295 | margin-top: 0;
296 | margin-bottom: 1rem;
297 | }
298 |
299 | ol ol,
300 | ul ul,
301 | ol ul,
302 | ul ol {
303 | margin-bottom: 0;
304 | }
305 |
306 | dt {
307 | font-weight: 700;
308 | }
309 |
310 | dd {
311 | margin-bottom: 0.5rem;
312 | margin-left: 0;
313 | }
314 |
315 | blockquote {
316 | margin: 0 0 1rem;
317 | }
318 |
319 | b,
320 | strong {
321 | font-weight: bolder;
322 | }
323 |
324 | small {
325 | font-size: 0.875em;
326 | }
327 |
328 | mark {
329 | padding: 0.1875em;
330 | color: var(--bs-highlight-color);
331 | background-color: var(--bs-highlight-bg);
332 | }
333 |
334 | sub,
335 | sup {
336 | position: relative;
337 | font-size: 0.75em;
338 | line-height: 0;
339 | vertical-align: baseline;
340 | }
341 |
342 | sub {
343 | bottom: -0.25em;
344 | }
345 |
346 | sup {
347 | top: -0.5em;
348 | }
349 |
350 | a {
351 | color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
352 | text-decoration: underline;
353 | }
354 | a:hover {
355 | --bs-link-color-rgb: var(--bs-link-hover-color-rgb);
356 | }
357 |
358 | a:not([href]):not([class]), a:not([href]):not([class]):hover {
359 | color: inherit;
360 | text-decoration: none;
361 | }
362 |
363 | pre,
364 | code,
365 | kbd,
366 | samp {
367 | font-family: var(--bs-font-monospace);
368 | font-size: 1em;
369 | }
370 |
371 | pre {
372 | display: block;
373 | margin-top: 0;
374 | margin-bottom: 1rem;
375 | overflow: auto;
376 | font-size: 0.875em;
377 | }
378 | pre code {
379 | font-size: inherit;
380 | color: inherit;
381 | word-break: normal;
382 | }
383 |
384 | code {
385 | font-size: 0.875em;
386 | color: var(--bs-code-color);
387 | word-wrap: break-word;
388 | }
389 | a > code {
390 | color: inherit;
391 | }
392 |
393 | kbd {
394 | padding: 0.1875rem 0.375rem;
395 | font-size: 0.875em;
396 | color: var(--bs-body-bg);
397 | background-color: var(--bs-body-color);
398 | border-radius: 0.25rem;
399 | }
400 | kbd kbd {
401 | padding: 0;
402 | font-size: 1em;
403 | }
404 |
405 | figure {
406 | margin: 0 0 1rem;
407 | }
408 |
409 | img,
410 | svg {
411 | vertical-align: middle;
412 | }
413 |
414 | table {
415 | caption-side: bottom;
416 | border-collapse: collapse;
417 | }
418 |
419 | caption {
420 | padding-top: 0.5rem;
421 | padding-bottom: 0.5rem;
422 | color: var(--bs-secondary-color);
423 | text-align: left;
424 | }
425 |
426 | th {
427 | text-align: inherit;
428 | text-align: -webkit-match-parent;
429 | }
430 |
431 | thead,
432 | tbody,
433 | tfoot,
434 | tr,
435 | td,
436 | th {
437 | border-color: inherit;
438 | border-style: solid;
439 | border-width: 0;
440 | }
441 |
442 | label {
443 | display: inline-block;
444 | }
445 |
446 | button {
447 | border-radius: 0;
448 | }
449 |
450 | button:focus:not(:focus-visible) {
451 | outline: 0;
452 | }
453 |
454 | input,
455 | button,
456 | select,
457 | optgroup,
458 | textarea {
459 | margin: 0;
460 | font-family: inherit;
461 | font-size: inherit;
462 | line-height: inherit;
463 | }
464 |
465 | button,
466 | select {
467 | text-transform: none;
468 | }
469 |
470 | [role=button] {
471 | cursor: pointer;
472 | }
473 |
474 | select {
475 | word-wrap: normal;
476 | }
477 | select:disabled {
478 | opacity: 1;
479 | }
480 |
481 | [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
482 | display: none !important;
483 | }
484 |
485 | button,
486 | [type=button],
487 | [type=reset],
488 | [type=submit] {
489 | -webkit-appearance: button;
490 | }
491 | button:not(:disabled),
492 | [type=button]:not(:disabled),
493 | [type=reset]:not(:disabled),
494 | [type=submit]:not(:disabled) {
495 | cursor: pointer;
496 | }
497 |
498 | ::-moz-focus-inner {
499 | padding: 0;
500 | border-style: none;
501 | }
502 |
503 | textarea {
504 | resize: vertical;
505 | }
506 |
507 | fieldset {
508 | min-width: 0;
509 | padding: 0;
510 | margin: 0;
511 | border: 0;
512 | }
513 |
514 | legend {
515 | float: left;
516 | width: 100%;
517 | padding: 0;
518 | margin-bottom: 0.5rem;
519 | font-size: calc(1.275rem + 0.3vw);
520 | line-height: inherit;
521 | }
522 | @media (min-width: 1200px) {
523 | legend {
524 | font-size: 1.5rem;
525 | }
526 | }
527 | legend + * {
528 | clear: left;
529 | }
530 |
531 | ::-webkit-datetime-edit-fields-wrapper,
532 | ::-webkit-datetime-edit-text,
533 | ::-webkit-datetime-edit-minute,
534 | ::-webkit-datetime-edit-hour-field,
535 | ::-webkit-datetime-edit-day-field,
536 | ::-webkit-datetime-edit-month-field,
537 | ::-webkit-datetime-edit-year-field {
538 | padding: 0;
539 | }
540 |
541 | ::-webkit-inner-spin-button {
542 | height: auto;
543 | }
544 |
545 | [type=search] {
546 | -webkit-appearance: textfield;
547 | outline-offset: -2px;
548 | }
549 |
550 | /* rtl:raw:
551 | [type="tel"],
552 | [type="url"],
553 | [type="email"],
554 | [type="number"] {
555 | direction: ltr;
556 | }
557 | */
558 | ::-webkit-search-decoration {
559 | -webkit-appearance: none;
560 | }
561 |
562 | ::-webkit-color-swatch-wrapper {
563 | padding: 0;
564 | }
565 |
566 | ::-webkit-file-upload-button {
567 | font: inherit;
568 | -webkit-appearance: button;
569 | }
570 |
571 | ::file-selector-button {
572 | font: inherit;
573 | -webkit-appearance: button;
574 | }
575 |
576 | output {
577 | display: inline-block;
578 | }
579 |
580 | iframe {
581 | border: 0;
582 | }
583 |
584 | summary {
585 | display: list-item;
586 | cursor: pointer;
587 | }
588 |
589 | progress {
590 | vertical-align: baseline;
591 | }
592 |
593 | [hidden] {
594 | display: none !important;
595 | }
596 |
597 | /*# sourceMappingURL=bootstrap-reboot.css.map */
--------------------------------------------------------------------------------