feat: Add complete portfolio website with CV, resume, and project details
- Created main HTML templates for CV, resume, home, and project pages. - Implemented navigation bar and footer fragments for consistent layout. - Added sections for professional profile, qualifications, employment history, technical skills, and interests in CV. - Developed responsive design using Bootstrap and custom CSS. - Integrated dynamic project details with tech stack and documentation sections. - Established contact form functionality in the home page. - Included unit tests for application context loading.
This commit is contained in:
commit
7bd4a4fe3e
|
|
@ -0,0 +1,2 @@
|
|||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
HELP.md
|
||||
target/
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
src/main/resources/application.properties
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.0.1</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>Portfolio</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>Portfolio</name>
|
||||
<description>Website Portfolio for Kiyan Mckop</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>25</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.wimdeblauwe</groupId>
|
||||
<artifactId>htmx-spring-boot-thymeleaf</artifactId>
|
||||
<version>5.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.commonmark</groupId>
|
||||
<artifactId>commonmark</artifactId>
|
||||
<version>0.22.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.example.Portfolio.Component;
|
||||
|
||||
import org.commonmark.parser.Parser;
|
||||
import org.commonmark.renderer.html.HtmlRenderer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MarkdownService {
|
||||
|
||||
private final Parser parser = Parser.builder().build();
|
||||
private final HtmlRenderer renderer = HtmlRenderer.builder().build();
|
||||
|
||||
public String toHtml(String markdown) {
|
||||
return renderer.render(parser.parse(markdown));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.example.Portfolio.Configs;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
@Configuration
|
||||
public class GiteaClientConfig {
|
||||
|
||||
@Bean
|
||||
public WebClient giteaWebClient(
|
||||
@Value("${gitea.api.url}") String baseUrl,
|
||||
@Value("${gitea.token}") String token) {
|
||||
|
||||
return WebClient.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.defaultHeader(HttpHeaders.AUTHORIZATION, "token " + token)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.example.Portfolio.Controllers;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class CvController {
|
||||
|
||||
|
||||
|
||||
@GetMapping("/cv")
|
||||
private String CvPage(Model model){
|
||||
|
||||
return "cv";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.example.Portfolio.Controllers;
|
||||
|
||||
import com.example.Portfolio.DTO.Link;
|
||||
import com.example.Portfolio.Services.EmailService;
|
||||
import com.example.Portfolio.Services.GiteaRepoService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
public class Home {
|
||||
|
||||
@Autowired
|
||||
private GiteaRepoService giteaRepoService;
|
||||
@Autowired
|
||||
private EmailService emailService;
|
||||
|
||||
|
||||
|
||||
@GetMapping("/")
|
||||
public String home(Model model) {
|
||||
|
||||
//navbar links
|
||||
model.addAttribute("links", List.of(
|
||||
new Link("About", "#about"),
|
||||
new Link("Experience", "#experience"),
|
||||
new Link("Education", "#education"),
|
||||
new Link("Projects", "#projects"),
|
||||
new Link("Contact", "#contact")
|
||||
));
|
||||
|
||||
model.addAttribute(
|
||||
"projects",
|
||||
giteaRepoService.getRepositories("kiyan")
|
||||
.stream()
|
||||
.filter(repo -> !repo.fork() && !repo.isPrivate())
|
||||
.toList()
|
||||
);
|
||||
|
||||
return "home";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@PostMapping("/contact")
|
||||
public String handleContact(Model model,
|
||||
@RequestParam String name,
|
||||
@RequestParam String email,
|
||||
@RequestParam String message
|
||||
) {
|
||||
emailService.sendContactEmail(name, email, message);
|
||||
|
||||
String alertMessage = "Thank you! Your message has been sent.";
|
||||
model.addAttribute("alertMessage", alertMessage);
|
||||
return "home :: alertSuccess";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.example.Portfolio.Controllers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
||||
import com.example.Portfolio.DTO.Link;
|
||||
import com.example.Portfolio.Services.ProjectService;
|
||||
|
||||
@Controller
|
||||
public class ProjectController {
|
||||
|
||||
@Autowired
|
||||
ProjectService projectService;
|
||||
|
||||
@GetMapping("/project/{repo}")
|
||||
public String projectPage(@PathVariable String repo, Model model) {
|
||||
|
||||
//navbar links
|
||||
model.addAttribute("links", List.of(
|
||||
new Link("Overview", "#project-overview"),
|
||||
new Link("Documentation", "#documentation"),
|
||||
new Link("Roadmap", "#future-plans"),
|
||||
new Link("Demo-Vid", "#demo-vid")
|
||||
));
|
||||
|
||||
model.addAttribute("project", projectService.getProject(repo));
|
||||
|
||||
return "project";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.example.Portfolio.Controllers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import com.example.Portfolio.DTO.Education;
|
||||
import com.example.Portfolio.DTO.Job;
|
||||
import com.example.Portfolio.DTO.Project;
|
||||
import com.example.Portfolio.DTO.ResumeDto;
|
||||
import com.example.Portfolio.DTO.SkillGroup;
|
||||
|
||||
@Controller
|
||||
public class ResumeController {
|
||||
|
||||
@GetMapping("/resume")
|
||||
public String showResume(Model model) {
|
||||
ResumeDto resume = new ResumeDto(
|
||||
"Kiyan",
|
||||
"Mckop",
|
||||
"Software Engineer",
|
||||
"082 213 7232",
|
||||
"kiyanmckop395@gmail.com",
|
||||
"https://www.linkedin.com/in/kiyan-mckop-78a67b232/",
|
||||
"15 1st Avenue Marlands, Germiston, 1401",
|
||||
"""
|
||||
I am a driven and curious individual with a passion for
|
||||
solving problems and bringing creative ideas to life.
|
||||
As an Honours graduate in Information Technology with
|
||||
an emphasis in Software Engineering, I approach
|
||||
challenges with a blend of determination, adaptability,
|
||||
and a focus on continuous growth. I thrive in collaborative
|
||||
environments, where I can contribute my skills while also
|
||||
learning from others.
|
||||
""",
|
||||
List.of(
|
||||
new SkillGroup("Languages", "Java, Python, MySQL, HTML & CSS"),
|
||||
new SkillGroup("Tools", "Spring Boot, HTMX, Kubernetes"),
|
||||
new SkillGroup("Soft Skills",
|
||||
"Communication, Teamwork, Problem-Solving, Adaptability")),
|
||||
List.of(
|
||||
new Job("Graduate Software Engineer", "WBHO Construction",
|
||||
"Wynberg, Sandton", "2025",
|
||||
List.of("Developed and maintained internal ERP software systems.")),
|
||||
new Job("Data Capturer", "Wealth Associates", "Bryanston, Sandton",
|
||||
"2022",
|
||||
List.of("Accurate data capturing with a focus on confidentiality and integrity.")),
|
||||
new Job("General Office Work", "Wealth Associates",
|
||||
"Bryanston, Sandton", "2019",
|
||||
List.of("Administrative and operational support across office functions."))),
|
||||
List.of(
|
||||
new Project("PhotoGallery", "Java, Spring Boot, HTMX, MySQL", "2025",
|
||||
"https:mckopserver.ddns.net/photogallery"),
|
||||
new Project("TaskManager", "Java, Spring Boot, HTMX, MySQL", "2025",
|
||||
"https:mckopserver.ddns.net/taskmanager"),
|
||||
new Project("Portfolio Website", "Java, Spring Boot, Thymeleaf, HTML & CSS", "2025",
|
||||
"https:mckopserver.ddns.net/portfolio")),
|
||||
List.of(
|
||||
new Education("BScHons IT", "Eduvos", "Midrand", "2024",
|
||||
"Specialized in software engineering and algorithms, with a focus on developing applications and understanding core concepts."),
|
||||
new Education("BSc IT", "Eduvos", "Bedfordview", "2021-2023",
|
||||
"Specialized in software engineering and algorithms, with a focus on developing applications and understanding core concepts."),
|
||||
new Education("Matric", "Edenvale High School", "Edenvale", "2016-2020",
|
||||
"Major subjects included Information Technology, Business Studies, and Computer Applications Technology. ")));
|
||||
|
||||
model.addAttribute("resume", resume);
|
||||
return "resume";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.example.Portfolio.DTO;
|
||||
|
||||
public record Education(
|
||||
|
||||
String degree,
|
||||
String university,
|
||||
String location,
|
||||
String year,
|
||||
String relevantCoursework
|
||||
|
||||
) {}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.example.Portfolio.DTO;
|
||||
|
||||
public record GiteaContentDto(
|
||||
String name,
|
||||
String type,
|
||||
String download_url
|
||||
) {}
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.example.Portfolio.DTO;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public record GiteaRepoDto(
|
||||
String name,
|
||||
String description,
|
||||
String html_url,
|
||||
String default_branch,
|
||||
String website,
|
||||
boolean fork,
|
||||
@JsonProperty("private") boolean isPrivate
|
||||
) {}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.example.Portfolio.DTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record Job(
|
||||
|
||||
String title,
|
||||
String company,
|
||||
String location,
|
||||
String dateRange,
|
||||
List<String> achievements
|
||||
|
||||
) {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.example.Portfolio.DTO;
|
||||
|
||||
public record Link(
|
||||
String title, String directory
|
||||
){}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.example.Portfolio.DTO;
|
||||
|
||||
public record Project(
|
||||
|
||||
String name,
|
||||
String techStack,
|
||||
String date,
|
||||
String link
|
||||
|
||||
) {}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.example.Portfolio.DTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record ProjectView(
|
||||
String name,
|
||||
String description,
|
||||
String githubUrl,
|
||||
String liveUrl,
|
||||
|
||||
String overviewHtml,
|
||||
String documentationHtml,
|
||||
String futureHtml,
|
||||
String stackHtml,
|
||||
|
||||
List<String> screenshots
|
||||
) {}
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.example.Portfolio.DTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record ResumeDto (
|
||||
String firstName,
|
||||
String lastName,
|
||||
String jobTitle,
|
||||
String phone,
|
||||
String email,
|
||||
String linkedinUrl,
|
||||
String address,
|
||||
|
||||
String summary,
|
||||
|
||||
List<SkillGroup> skills,
|
||||
List<Job> experiences,
|
||||
List<Project> projects,
|
||||
List<Education> education
|
||||
|
||||
|
||||
) {}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.example.Portfolio.DTO;
|
||||
|
||||
public record SkillGroup(
|
||||
String category,
|
||||
String items
|
||||
) {}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.example.Portfolio;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class PortfolioApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(PortfolioApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.example.Portfolio.Services;
|
||||
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class EmailService {
|
||||
|
||||
@Autowired
|
||||
private JavaMailSender mailSender;
|
||||
|
||||
public void sendContactEmail(String name, String email, String message) {
|
||||
SimpleMailMessage mailMessage = new SimpleMailMessage();
|
||||
mailMessage.setTo("kiyanmckop395@gmail.com");
|
||||
mailMessage.setSubject("New Contact Form Submission from " + name);
|
||||
mailMessage.setText(
|
||||
"Name: " + name + "\n" +
|
||||
"Email: " + email + "\n\n" +
|
||||
"Message:\n" + message
|
||||
);
|
||||
mailSender.send(mailMessage);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.example.Portfolio.Services;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import com.example.Portfolio.DTO.GiteaContentDto;
|
||||
import com.example.Portfolio.DTO.GiteaRepoDto;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Service
|
||||
public class GiteaRepoService {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
public GiteaRepoService(WebClient webClient) {
|
||||
this.webClient = webClient;
|
||||
}
|
||||
|
||||
public List<GiteaRepoDto> getRepositories(String username) {
|
||||
return webClient.get()
|
||||
.uri(uriBuilder -> uriBuilder
|
||||
.path("/users/{user}/repos")
|
||||
.queryParam("visibility", "public")
|
||||
.build(username))
|
||||
.retrieve()
|
||||
.bodyToFlux(GiteaRepoDto.class)
|
||||
.collectList()
|
||||
.block();
|
||||
}
|
||||
|
||||
public GiteaRepoDto getRepository(String owner, String repo) {
|
||||
|
||||
return webClient.get()
|
||||
.uri("/repos/{owner}/{repo}", owner, repo)
|
||||
.retrieve()
|
||||
.onStatus(
|
||||
status -> status.value() == 404,
|
||||
response -> Mono.error(
|
||||
new IllegalArgumentException("Repository not found: " + repo)))
|
||||
.bodyToMono(GiteaRepoDto.class)
|
||||
.block();
|
||||
}
|
||||
|
||||
public String getRawFile(
|
||||
String owner,
|
||||
String repo,
|
||||
String branch,
|
||||
String path) {
|
||||
|
||||
return webClient.get()
|
||||
.uri("/repos/{o}/{r}/raw/{b}/{p}",
|
||||
owner, repo, branch, path)
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block();
|
||||
}
|
||||
|
||||
public List<String> getScreenshots(String owner, String repo) {
|
||||
return webClient.get()
|
||||
.uri("/repos/{o}/{r}/contents/screenshots", owner, repo)
|
||||
.retrieve()
|
||||
.bodyToFlux(GiteaContentDto.class)
|
||||
.filter(item -> item.type().equals("file"))
|
||||
.map(GiteaContentDto::download_url)
|
||||
.toStream()
|
||||
.toList();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.example.Portfolio.Services;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.example.Portfolio.Component.MarkdownService;
|
||||
import com.example.Portfolio.DTO.ProjectView;
|
||||
|
||||
@Service
|
||||
public class ProjectService {
|
||||
|
||||
private final GiteaRepoService giteaService;
|
||||
private final MarkdownService markdownService;
|
||||
|
||||
public ProjectService(GiteaRepoService giteaService,
|
||||
MarkdownService markdownService) {
|
||||
this.giteaService = giteaService;
|
||||
this.markdownService = markdownService;
|
||||
}
|
||||
|
||||
public ProjectView getProject(String repo) {
|
||||
|
||||
var repoMeta = giteaService.getRepository("kiyan", repo);
|
||||
|
||||
String overview = markdownService.toHtml(
|
||||
giteaService.getRawFile("kiyan", repo, "main", "README.md"));
|
||||
|
||||
String docs = markdownService.toHtml(
|
||||
giteaService.getRawFile("kiyan", repo, "main", "documentation.md"));
|
||||
|
||||
String future = markdownService.toHtml(
|
||||
giteaService.getRawFile("kiyan", repo, "main", "future.md"));
|
||||
|
||||
String stack = markdownService.toHtml(
|
||||
giteaService.getRawFile("kiyan", repo, "main", "stack.md"));
|
||||
|
||||
List<String> screenshots = giteaService.getScreenshots("kiyan", repo);
|
||||
|
||||
return new ProjectView(
|
||||
repoMeta.name(),
|
||||
repoMeta.description(),
|
||||
repoMeta.html_url(),
|
||||
repoMeta.website(),
|
||||
overview,
|
||||
docs,
|
||||
future,
|
||||
stack,
|
||||
screenshots);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,166 @@
|
|||
:root {
|
||||
--primary: #1f2933;
|
||||
--secondary: #4b5563;
|
||||
--light: #f9fafb;
|
||||
--accent: #2563eb;
|
||||
--border: #e5e7eb;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--light);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, Helvetica, Arial, sans-serif;
|
||||
color: var(--primary);
|
||||
line-height: 1.65;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 210mm;
|
||||
min-height: 297mm;
|
||||
margin: 24px auto;
|
||||
padding: 45px 55px;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.page:last-child {
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
/* Headings */
|
||||
h1 {
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
margin-top: 42px;
|
||||
margin-bottom: 18px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.2px;
|
||||
color: var(--accent);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Text */
|
||||
p {
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 13px;
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 8px 0 0 18px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 6px;
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 22px;
|
||||
margin-bottom: 35px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
max-width: 65%;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
color: var(--secondary);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
color: var(--secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Grids */
|
||||
.section-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: 50px;
|
||||
row-gap: 14px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
.skills {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px 50px;
|
||||
}
|
||||
|
||||
/* Entries */
|
||||
.entry {
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.entry p strong {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
margin-top: 70px;
|
||||
font-size: 13px;
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
/* Print */
|
||||
@media print {
|
||||
body {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.page {
|
||||
margin: 0;
|
||||
border: none;
|
||||
width: auto;
|
||||
min-height: auto;
|
||||
padding: 40px 50px;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
body {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 4rem 0;
|
||||
border-bottom: 1px solid var(--bs-secondary-bg-subtle);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* ---------- NAVBAR ---------- */
|
||||
.navbar {
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-link::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -4px;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background-color: var(--bs-primary);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ---------- ICON LINKS ---------- */
|
||||
.icon-link i {
|
||||
font-size: 1.6rem;
|
||||
transition: color 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.icon-link:hover i {
|
||||
color: var(--bs-primary);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
/* ---------- INTRO / HERO ---------- */
|
||||
.intro {
|
||||
min-height: 70vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(180deg, var(--bs-light), #ffffff);
|
||||
}
|
||||
|
||||
.intro img {
|
||||
border: 4px solid var(--bs-white);
|
||||
}
|
||||
|
||||
.skill-badge {
|
||||
background: var(--bs-secondary-bg);
|
||||
border-radius: 2rem;
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ---------- CARDS (GLOBAL) ---------- */
|
||||
.card,
|
||||
.project-item,
|
||||
.contact-box {
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--bs-secondary-bg-subtle);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
background: var(--bs-white);
|
||||
}
|
||||
|
||||
.card:hover,
|
||||
.project-item:hover,
|
||||
.contact-box:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* ---------- SECTION HEADERS ---------- */
|
||||
.section-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.section-header i {
|
||||
font-size: 2rem;
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
/* ---------- FOOTER ---------- */
|
||||
footer a {
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
footer i {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
body {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 5rem 0;
|
||||
border-bottom: 1px solid var(--bs-secondary-bg-subtle);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.hero {
|
||||
min-height: 65vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero img {
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
/* ---------- NAVBAR ---------- */
|
||||
.navbar {
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-link::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -4px;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background-color: var(--bs-primary);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-link::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -4px;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background-color: var(--bs-primary);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.icon-link i {
|
||||
font-size: 1.5rem;
|
||||
transition: color 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.icon-link:hover i {
|
||||
color: var(--bs-primary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
|
||||
.card,
|
||||
.project-box {
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--bs-secondary-bg-subtle);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover,
|
||||
.project-box:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.tech-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tech-chip {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-radius: 999px;
|
||||
background-color: #f1f3f5;
|
||||
color: #495057;
|
||||
border: 1px solid #dee2e6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
@page {
|
||||
size: A4;
|
||||
margin: 0.5in;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Calibri", "Roboto", "Helvetica Neue", Arial, sans-serif;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
/* Tighter line spacing */
|
||||
background-color: #fff;
|
||||
padding: 0;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* HEADER - Tighter vertical profile */
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: "Georgia", "Times New Roman", serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
font-size: 28px;
|
||||
margin-bottom: 4px;
|
||||
color: #111;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
font-size: 12px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0 6px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* SECTIONS - Reduced margin */
|
||||
.section {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
h2.section-title {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.2px;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 3px;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ENTRIES - Tighter spacing between items */
|
||||
.entry {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.entry-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.entry-date {
|
||||
font-family: "Georgia", serif;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* LISTS - Reduced bullet spacing */
|
||||
p {
|
||||
font-size: 13.5px;
|
||||
margin-bottom: 6px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: 2px;
|
||||
padding-left: 18px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 13.5px;
|
||||
margin-bottom: 2px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.skills-list li {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.skill-category {
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
margin-right: 4px;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.navbar {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 6.8 MiB |
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,196 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="light" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Curriculum Vitae | Software Engineer Portfolio</title>
|
||||
|
||||
<meta name="description" content="Software Engineer specializing in Java and Spring Boot">
|
||||
<meta name="author" content="Kiyan Mckop">
|
||||
|
||||
<link th:href="@{/assets/css/bootstrap.min.css}" rel="stylesheet">
|
||||
<link th:href="@{https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css}" rel="stylesheet">
|
||||
<link rel="stylesheet" th:href="@{/assets/css/cv.css}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<nav th:fragment="navbar" class="navbar navbar-expand-md sticky-top bg-body border-bottom">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" th:href="@{/}">Kiyan Mckop</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMain">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navMain">
|
||||
<ul class="navbar-nav ms-auto align-items-md-center gap-md-2">
|
||||
<li class="nav-item">
|
||||
<a class="btn" onclick="window.print()"><i class="bi bi-download"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- PAGE 1: Header + Professional Profile + Personal Info -->
|
||||
<section class="page">
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<h1>Kiyan Jodie Mckop</h1>
|
||||
<div class="subtitle">Software Engineer</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
Germiston, South Africa<br>
|
||||
082 213 7232<br>
|
||||
Kiyanmckop395@gmail.com<br>
|
||||
South African
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<h2>Professional Profile</h2>
|
||||
<p>
|
||||
Detail-oriented and motivated Graduate Software Engineer with a strong academic
|
||||
foundation in software engineering and practical industry exposure. Proven ability
|
||||
to work effectively in collaborative environments, adapt quickly to new technologies,
|
||||
and deliver reliable solutions under pressure. Demonstrates a disciplined work ethic,
|
||||
strong problem-solving skills, and a continuous desire to improve both technically
|
||||
and professionally.
|
||||
</p>
|
||||
|
||||
<h2>Personal Information</h2>
|
||||
<div class="section-grid">
|
||||
<div><span class="label">Full Name:</span> <span class="value">Kiyan Jodie Mckop</span></div>
|
||||
<div><span class="label">Date of Birth:</span> <span class="value">03 October 2002</span></div>
|
||||
<div><span class="label">Identity Number:</span> <span class="value">0210036379084</span></div>
|
||||
<div><span class="label">Nationality:</span> <span class="value">South African</span></div>
|
||||
<div><span class="label">Home Language:</span> <span class="value">English</span></div>
|
||||
<div><span class="label">Other Languages:</span> <span class="value">None</span></div>
|
||||
<div><span class="label">Driver's License:</span> <span class="value">Yes</span></div>
|
||||
<div><span class="label">Residential Address:</span> <span class="value">15 1st Avenue, Marlands,
|
||||
Germiston</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PAGE 2: Formal Qualifications + Certifications -->
|
||||
<section class="page">
|
||||
<h2>Formal Qualifications</h2>
|
||||
|
||||
<div class="entry">
|
||||
<h3>Eduvos</h3>
|
||||
<small>2024</small>
|
||||
<p><strong>BSc (Honours) in Information Technology (Software Engineering)</strong></p>
|
||||
<p>
|
||||
Advanced studies in software architecture, systems analysis, applied research,
|
||||
and modern development methodologies, completed with distinction (Cum Laude).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="entry">
|
||||
<h3>Eduvos</h3>
|
||||
<small>2020 - 2023</small>
|
||||
<p><strong>BSc in Information Technology (Software Engineering)</strong></p>
|
||||
<p>
|
||||
Comprehensive grounding in software development principles, object-oriented
|
||||
programming, databases, systems design, and software testing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="entry">
|
||||
<h3>Edenvale High School</h3>
|
||||
<small>2016 - 2020</small>
|
||||
<p><strong>National Senior Certificate (Matric)</strong></p>
|
||||
<p>
|
||||
Major subjects included Information Technology, Business Studies,
|
||||
and Computer Applications Technology.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>Certifications</h2>
|
||||
<ul>
|
||||
<li>Yenza Izinto - Microsoft Digital Literacy Certification</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- PAGE 3: Employment History -->
|
||||
<section class="page">
|
||||
<h2>Employment History</h2>
|
||||
|
||||
<div class="entry">
|
||||
<h3>WBHO Construction</h3>
|
||||
<small>February 2025 - December 2025</small>
|
||||
<p><strong>Graduate Software Engineer</strong></p>
|
||||
<p>
|
||||
Participated in the design, development, and maintenance of internal software
|
||||
solutions. Collaborated with senior engineers and cross-functional teams to
|
||||
analyse requirements, implement features, test systems, and resolve defects.
|
||||
Gained practical exposure to enterprise development standards and professional
|
||||
software delivery practices.
|
||||
</p>
|
||||
<p><em>Reason for leaving:</em> Graduate contract completed.</p>
|
||||
</div>
|
||||
|
||||
<div class="entry">
|
||||
<h3>Wealth Associates</h3>
|
||||
<small>February 2022 - March 2022</small>
|
||||
<p><strong>Short-Term Insurance Data Capturer</strong></p>
|
||||
<p>
|
||||
Accurately captured and maintained insurance policy data, ensuring compliance
|
||||
with internal quality standards. Demonstrated attention to detail and efficiency
|
||||
in a fast-paced administrative environment.
|
||||
</p>
|
||||
<p><em>Reason for leaving:</em> Returned to studies (holiday position).</p>
|
||||
</div>
|
||||
|
||||
<div class="entry">
|
||||
<h3>Wealth Associates</h3>
|
||||
<small>December 2019 - January 2020</small>
|
||||
<p><strong>General Office Assistant</strong></p>
|
||||
<p>
|
||||
Provided general administrative support, filing, data handling, and office
|
||||
assistance during peak periods.
|
||||
</p>
|
||||
<p><em>Reason for leaving:</em> Returned to school (holiday position).</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PAGE 4: Technical Skills + Interests + Declaration -->
|
||||
<section class="page">
|
||||
<h2>Technical Skills</h2>
|
||||
<div class="skills">
|
||||
<div>Software Development Principles</div>
|
||||
<div>Object-Oriented Programming</div>
|
||||
<div>Database Fundamentals</div>
|
||||
<div>System Analysis & Design</div>
|
||||
<div>Version Control (Git)</div>
|
||||
<div>Problem Solving & Debugging</div>
|
||||
<div>Agile & Team-Based Development</div>
|
||||
<div>Documentation & Reporting</div>
|
||||
</div>
|
||||
|
||||
<h2>Interests</h2>
|
||||
<ul>
|
||||
<li>Photography and photo editing</li>
|
||||
<li>Programming and personal technical projects</li>
|
||||
<li>Fishing and camping</li>
|
||||
<li>Outdoor and hands-on activities</li>
|
||||
</ul>
|
||||
|
||||
<h2>Declaration</h2>
|
||||
<p>
|
||||
I hereby declare that all information provided in this Curriculum Vitae is accurate
|
||||
and true.
|
||||
</p>
|
||||
|
||||
<div class="footer">
|
||||
<p>Signed: Kiyan Jodie Mckop</p>
|
||||
<p>Date: 01 January 2026</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script th:src="@{/assets/js/bootstrap.bundle.min.js}"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<footer th:fragment="footer" class="py-4 border-top">
|
||||
<div class="container text-center">
|
||||
<p class="mb-2 fw-semibold">Kiyan Mckop</p>
|
||||
|
||||
<!-- Social Icons -->
|
||||
<div class="d-flex justify-content-center gap-4 mb-3">
|
||||
<a th:href="@{https://github.com/KiyanMckop}" target="_blank" class="icon-link" title="GitHub">
|
||||
<i class="bi bi-github"></i>
|
||||
</a>
|
||||
|
||||
<a th:href="@{https://www.linkedin.com/in/kiyan-mckop-78a67b232/}" target="_blank" class="icon-link"
|
||||
title="LinkedIn">
|
||||
<i class="bi bi-linkedin"></i>
|
||||
</a>
|
||||
|
||||
<a th:href="@{https://www.instagram.com/kiyan_mckop/}" target="_blank" class="icon-link" title="Instagram">
|
||||
<i class="bi bi-instagram"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="text-muted small mb-0">
|
||||
© 2025 | Software Engineer Portfolio
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<nav th:fragment="navbar" class="navbar navbar-expand-md sticky-top bg-body border-bottom">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" th:href="@{/}">Kiyan Mckop</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMain">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navMain">
|
||||
<ul class="navbar-nav ms-auto align-items-md-center gap-md-2">
|
||||
<li th:each="link : ${links}" class="nav-item">
|
||||
<a class="nav-link" th:href="${link.directory}" th:text="${link.title}">About</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="light" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Kiyan Mckop | Software Engineer Portfolio</title>
|
||||
|
||||
<meta name="description" content="Software Engineer specializing in Java and Spring Boot">
|
||||
<meta name="author" content="Kiyan Mckop">
|
||||
|
||||
<link th:href="@{/assets/css/bootstrap.min.css}" rel="stylesheet">
|
||||
<link th:href="@{https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css}" rel="stylesheet">
|
||||
<link rel="stylesheet" th:href="@{/assets/css/home.css}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- NAVBAR -->
|
||||
<div th:replace="~{fragments/navbar :: navbar}"></div>
|
||||
|
||||
<!-- INTRO / HERO + ABOUT -->
|
||||
<header id="about" class="intro">
|
||||
<div class="container">
|
||||
<div class="row align-items-center gy-4">
|
||||
|
||||
<div class="col-md-4 text-center">
|
||||
<img th:src="@{/assets/images/profileImage.jpg}" class="rounded-circle mb-3" width="160"
|
||||
height="160" alt="Kiyan Mckop">
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<h1 class="fw-bold">Kiyan Mckop</h1>
|
||||
<p class="lead text-muted mb-3">
|
||||
Software Engineer specializing in Java, Spring Boot, and backend systems.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Honours graduate in Information Technology with experience building secure,
|
||||
maintainable enterprise software and internal ERP systems.
|
||||
</p>
|
||||
|
||||
<!-- Skills -->
|
||||
<div class="d-flex flex-wrap gap-2 my-3">
|
||||
<span class="skill-badge">Java</span>
|
||||
<span class="skill-badge">Spring Boot</span>
|
||||
<span class="skill-badge">REST APIs</span>
|
||||
<span class="skill-badge">SQL</span>
|
||||
<span class="skill-badge">Docker</span>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="d-flex gap-4 mt-3">
|
||||
<a class="icon-link" th:href="@{/resume}" title="Resume">
|
||||
<i class="bi bi-file-earmark-person"></i>
|
||||
</a>
|
||||
<a class="icon-link" th:href="@{/cv}"
|
||||
title="CV">
|
||||
<i class="bi bi-file-earmark-text"></i>
|
||||
</a>
|
||||
<a class="icon-link" href="https://mckopserver.ddns.net" target="_blank" title="Server">
|
||||
<i class="bi bi-hdd-network"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- EXPERIENCE -->
|
||||
<section id="experience">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-briefcase"></i>
|
||||
<h2 class="fw-bold mt-2">Work Experience</h2>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4" th:each="exp : ${null}">
|
||||
<!-- Static cards retained -->
|
||||
</div>
|
||||
|
||||
<!-- WBHO -->
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 p-3">
|
||||
<h5>Graduate Software Engineer</h5>
|
||||
<p class="text-muted">WBHO Construction · 2025</p>
|
||||
<p>Developed and maintained internal ERP software systems.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 p-3">
|
||||
<h5>Data Capturer</h5>
|
||||
<p class="text-muted">Wealth Associates · 2022</p>
|
||||
<p>Handled sensitive data with high accuracy and integrity.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 p-3">
|
||||
<h5>General Office Work</h5>
|
||||
<p class="text-muted">Wealth Associates · 2019</p>
|
||||
<p>Administrative and operational support.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- EDUCATION -->
|
||||
<section id="education" class="bg-light">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-mortarboard"></i>
|
||||
<h2 class="fw-bold mt-2">Education</h2>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card p-4 text-center">
|
||||
<h5>BSc Honours in Information Technology</h5>
|
||||
<p class="text-muted mb-1">Specialization: Software Engineering</p>
|
||||
<p class="text-muted">Graduated with Honours</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card p-4 text-center">
|
||||
<h5>BSc in Information Technology</h5>
|
||||
<p class="text-muted mb-1">Specialization: Software Engineering</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card p-4 text-center">
|
||||
<h5>Matric Certificate</h5>
|
||||
<p class="text-muted mb-1">Major Subjects: IT | CAT |Business</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PROJECTS -->
|
||||
<section id="projects">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-code-slash"></i>
|
||||
<h2 class="fw-bold mt-2">Projects</h2>
|
||||
<p class="text-muted">Selected technical work</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 d-inline-flex justify-content-center align-items-center" style="width: 100%;">
|
||||
<div class="col-md-6 col-lg-4" th:each="project : ${projects}">
|
||||
<div class="project-item h-100 p-3 ">
|
||||
<h5 th:text="${project.name}"></h5>
|
||||
<p class="small text-muted" th:text="${project.default_branch}"></p>
|
||||
<p th:text="${project.description}"></p>
|
||||
<a th:href="@{/project/{repo}(repo=${project.name})}">View Details</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CONTACT -->
|
||||
<section id="contact" class="bg-light">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-envelope"></i>
|
||||
<h2 class="fw-bold mt-2">Contact</h2>
|
||||
<p class="text-muted">Let’s work together</p>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="contact-box p-4">
|
||||
<form method="post" th:action="@{/contact}">
|
||||
<input class="form-control mb-3" name="name" placeholder="Name" required>
|
||||
<input type="email" class="form-control mb-3" name="email" placeholder="Email" required>
|
||||
<textarea class="form-control mb-3" name="message" rows="4" placeholder="Message"
|
||||
required></textarea>
|
||||
<button class="btn btn-primary w-100">Send Message</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div th:fragment="alertSuccess" role="alert" class='alert alert-success' th:text="${alertMessage}">Thank you! Your message has been sent.</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div th:replace="~{fragments/footer :: footer}"></div>
|
||||
|
||||
<script th:src="@{/assets/js/bootstrap.bundle.min.js}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="light" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<!-- ================= META ================= -->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title th:text="${project.name} + ' | Kiyan Mckop'">Project</title>
|
||||
|
||||
<!-- SEO -->
|
||||
<meta name="description" th:content="${project.description}">
|
||||
<meta name="author" content="Kiyan Mckop">
|
||||
|
||||
<link th:href="@{/assets/css/bootstrap.min.css}" rel="stylesheet">
|
||||
<link th:href="@{https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css}" rel="stylesheet">
|
||||
<link rel="stylesheet" th:href="@{/assets/css/project.css}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- ================= NAVBAR ================= -->
|
||||
<div th:replace="~{fragments/navbar :: navbar}"></div>
|
||||
|
||||
<!-- ================= HERO ================= -->
|
||||
<header class="hero bg-light">
|
||||
<div class="container text-center">
|
||||
<h1 class="fw-bold" th:text="${project.name}">Project Name</h1>
|
||||
|
||||
<p class="lead text-muted mt-2" th:text="${project.description}">
|
||||
Project description
|
||||
</p>
|
||||
|
||||
<!-- Project Links -->
|
||||
<div class="d-flex justify-content-center gap-4 mt-4">
|
||||
<a th:href="${project.githubUrl}" target="_blank" class="icon-link" title="GitHub Repository">
|
||||
<i class="bi bi-github"></i>
|
||||
</a>
|
||||
|
||||
<a th:if="${project.liveUrl != null}" th:href="${project.liveUrl}" target="_blank" class="icon-link"
|
||||
title="Live Application">
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ================= SCREENSHOTS ================= -->
|
||||
<section th:if="${project.screenshots != null && !project.screenshots.isEmpty()}">
|
||||
<div class="container">
|
||||
<h2 class="fw-bold text-center mb-5">Application Screenshots</h2>
|
||||
|
||||
<div id="projectCarousel" class="carousel slide" data-bs-ride="carousel">
|
||||
<div class="carousel-inner rounded-4 shadow-sm">
|
||||
<div th:each="img, stat : ${project.screenshots}"
|
||||
th:class="'carousel-item ' + (${stat.index} == 0 ? 'active' : '')">
|
||||
<img th:src="${img}" class="d-block w-100" alt="Project Screenshot">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#projectCarousel"
|
||||
data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon"></span>
|
||||
</button>
|
||||
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#projectCarousel"
|
||||
data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= OVERVIEW & STACK ================= -->
|
||||
<section id="project-overview" class="bg-light">
|
||||
<div class="container">
|
||||
<div class="row gy-4">
|
||||
|
||||
<!-- Overview -->
|
||||
<div class="col-lg-8">
|
||||
<h2 class="fw-bold">Project Overview</h2>
|
||||
<div th:utext="${project.overviewHtml}"></div>
|
||||
</div>
|
||||
|
||||
<!-- Stack -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-semibold mb-3">Tech Stack</h5>
|
||||
<div th:utext="${project.stackHtml}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= DOCUMENTATION ================= -->
|
||||
<section id="documentation" th:if="${project.documentationHtml != null}">
|
||||
<div class="container">
|
||||
<h2 class="fw-bold text-center mb-5">Documentation</h2>
|
||||
<div th:utext="${project.documentationHtml}"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= FUTURE PLANS ================= -->
|
||||
<section id="future-plans" class="bg-light" th:if="${project.futureHtml != null}">
|
||||
<div class="container">
|
||||
<h2 class="fw-bold text-center mb-5">Future Plans</h2>
|
||||
<div th:utext="${project.futureHtml}"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= DEMO ================= -->
|
||||
<section id="demo-vid">
|
||||
<div class="container">
|
||||
<h2 class="fw-bold text-center mb-5">Demo Video</h2>
|
||||
|
||||
<div class="ratio ratio-16x9 rounded-4 shadow-sm overflow-hidden text-center">
|
||||
<span><em>Coming Soon</em></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= FOOTER ================= -->
|
||||
<div th:replace="~{fragments/footer :: footer}"></div>
|
||||
|
||||
<script th:src="@{/assets/js/bootstrap.bundle.min.js}"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>resume | Software Engineer Portfolio</title>
|
||||
|
||||
<meta name="description" content="Software Engineer specializing in Java and Spring Boot">
|
||||
<meta name="author" content="Kiyan Mckop">
|
||||
|
||||
<link th:href="@{/assets/css/bootstrap.min.css}" rel="stylesheet">
|
||||
<link th:href="@{https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css}" rel="stylesheet">
|
||||
<link rel="stylesheet" th:href="@{/assets/css/resume.css}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<nav th:fragment="navbar" class="navbar navbar-expand-md sticky-top bg-body border-bottom">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" th:href="@{/}">Kiyan Mckop</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMain">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navMain">
|
||||
<ul class="navbar-nav ms-auto align-items-md-center gap-md-2">
|
||||
<li class="nav-item">
|
||||
<a class="btn" onclick="window.print()"><i class="bi bi-download"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<header>
|
||||
<h1 th:text="|${resume.firstName} ${resume.lastName}|">DANIEL WILSON</h1>
|
||||
<div class="subtitle" th:text="${resume.jobTitle}">SOFTWARE ENGINEER</div>
|
||||
|
||||
<div class="contact-info">
|
||||
<span th:if="${resume.phone}">
|
||||
<a th:href="'tel:' + ${resume.phone}" th:text="${resume.phone}"></a>
|
||||
</span>
|
||||
<span class="separator" th:if="${resume.phone}">•</span>
|
||||
|
||||
<span th:if="${resume.email}">
|
||||
<a th:href="'mailto:' + ${resume.email}" th:text="${resume.email}"></a>
|
||||
</span>
|
||||
<span class="separator" th:if="${resume.email}">•</span>
|
||||
|
||||
<span th:if="${resume.linkedinUrl}">
|
||||
<a th:href="${resume.linkedinUrl}">LinkedIn</a>
|
||||
</span>
|
||||
<span class="separator" th:if="${resume.linkedinUrl}">•</span>
|
||||
|
||||
<span th:text="${resume.address}">Address</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Professional Overview</h2>
|
||||
<p th:text="${resume.summary}">Summary text...</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Skills</h2>
|
||||
<ul class="list-unstyled skills-list">
|
||||
<li th:each="skillGroup : ${resume.skills}">
|
||||
<span class="skill-category" th:text="|${skillGroup.category}:|">Category:</span>
|
||||
<span th:text="${skillGroup.items}">Skill items...</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Work Experience</h2>
|
||||
<div class="entry" th:each="job : ${resume.experiences}">
|
||||
<div class="entry-header">
|
||||
<div>
|
||||
<span class="entry-title" th:text="${job.title}">Title</span>
|
||||
<span style="margin: 0 4px; color: #ccc;">|</span>
|
||||
<span class="company-name" th:text="|${job.company}, ${job.location}|">Company</span>
|
||||
</div>
|
||||
<div class="entry-date" th:text="${job.dateRange}">Date</div>
|
||||
</div>
|
||||
<ul>
|
||||
<li th:each="point : ${job.achievements}" th:text="${point}">Point</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Key Projects</h2>
|
||||
<div class="entry" th:each="project : ${resume.projects}">
|
||||
<div class="entry-header">
|
||||
<div>
|
||||
<span class="entry-title" th:text="${project.name}">Project Name</span>
|
||||
<span style="font-size: 0.85em; color: #666;" th:if="${project.techStack}"
|
||||
th:text="|(${project.techStack})|"></span>
|
||||
<a style="font-size: 0.8em; margin-left: 5px; color: #0d6efd;" th:if="${project.link}"
|
||||
th:href="${project.link}">[View]</a>
|
||||
</div>
|
||||
<div class="entry-date" th:if="${project.date}" th:text="${project.date}">Date</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Education</h2>
|
||||
<div class="entry" th:each="edu : ${resume.education}">
|
||||
<div class="entry-header">
|
||||
<div>
|
||||
<span class="entry-title" th:text="${edu.degree}">Degree</span>
|
||||
<span style="margin: 0 4px; color: #ccc;">|</span>
|
||||
<span class="company-name" th:text="${edu.university} + ' - ' + ${edu.location}">Institution</span>
|
||||
</div>
|
||||
<div class="entry-date" th:text="${edu.year}">Year</div>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #555;" th:if="${edu.relevantCoursework}">
|
||||
<strong>Relevant Coursework:</strong> <span th:text="${edu.relevantCoursework}">Courses</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script th:src="@{/assets/js/bootstrap.bundle.min.js}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.example.Portfolio;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class PortfolioApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue