├── .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 | DigitalOcean -------------------------------------------------------------------------------- /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 | Google Cloud -------------------------------------------------------------------------------- /public/img/skills/git.svg: -------------------------------------------------------------------------------- 1 | Git -------------------------------------------------------------------------------- /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 | R -------------------------------------------------------------------------------- /public/img/skills/javascript.svg: -------------------------------------------------------------------------------- 1 | JavaScript -------------------------------------------------------------------------------- /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 | Docker -------------------------------------------------------------------------------- /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 | OpenAPI Initiative -------------------------------------------------------------------------------- /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 */ --------------------------------------------------------------------------------