feat!: convert portfolio to Haskell web application
* Migrate from static HTML/JS to Haskell/Scotty web application * Add server-side routing and API endpoints * Implement language switching and command processing * Set up project structure with stack * Move static assets to dedicated directory * Add type definitions and template rendering
This commit is contained in:
parent
8b9f4f565a
commit
cdaf2c4157
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.stack-work/
|
||||||
|
*~
|
||||||
|
*.hi
|
||||||
|
*.o
|
||||||
|
*.cabal
|
||||||
|
dist
|
||||||
|
dist-*
|
||||||
|
cabal-dev
|
||||||
|
.ghc.environment.*
|
||||||
|
tags
|
||||||
26
app/Main.hs
Normal file
26
app/Main.hs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Web.Scotty
|
||||||
|
import Network.Wai.Middleware.Static
|
||||||
|
import Network.Wai.Middleware.RequestLogger
|
||||||
|
import Control.Monad.IO.Class
|
||||||
|
import Data.Text.Lazy (pack)
|
||||||
|
import System.Environment (getEnv)
|
||||||
|
import Control.Exception (catch)
|
||||||
|
import qualified Routes
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
port <- read <$> getEnv "PORT" `catch` \(_ :: IOError) -> return "3000"
|
||||||
|
putStrLn $ "Starting server on port " ++ show port
|
||||||
|
scotty port $ do
|
||||||
|
middleware logStdoutDev
|
||||||
|
middleware $ staticPolicy (addBase "static")
|
||||||
|
|
||||||
|
-- Routes
|
||||||
|
get "/" $ Routes.indexHandler
|
||||||
|
get "/api/command/:cmd" $ Routes.commandHandler
|
||||||
|
get "/api/language/:lang" $ Routes.languageHandler
|
||||||
113
app/Routes.hs
Normal file
113
app/Routes.hs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Routes where
|
||||||
|
|
||||||
|
import Web.Scotty
|
||||||
|
import Data.Text.Lazy (Text)
|
||||||
|
import qualified Data.Text.Lazy as TL
|
||||||
|
import Data.Map.Strict (Map)
|
||||||
|
import qualified Data.Map.Strict as Map
|
||||||
|
import Control.Monad.IO.Class
|
||||||
|
import Network.HTTP.Types.Status
|
||||||
|
import Data.Aeson (ToJSON, object, (.=))
|
||||||
|
import Templates
|
||||||
|
import Types
|
||||||
|
|
||||||
|
indexHandler :: ActionM ()
|
||||||
|
indexHandler = do
|
||||||
|
html $ renderIndex French
|
||||||
|
|
||||||
|
commandHandler :: ActionM ()
|
||||||
|
commandHandler = do
|
||||||
|
cmd <- param "cmd" :: ActionM Text
|
||||||
|
langParam <- (param "lang" :: ActionM Text) `rescue` (\_ -> return "fr")
|
||||||
|
let lang = if langParam == "en" then English else French
|
||||||
|
let response = processCommand cmd lang
|
||||||
|
json response
|
||||||
|
|
||||||
|
languageHandler :: ActionM ()
|
||||||
|
languageHandler = do
|
||||||
|
langParam <- param "lang" :: ActionM Text
|
||||||
|
let lang = if langParam == "en" then English else French
|
||||||
|
let response = CommandResponse (getTranslation "languageChanged" lang) False
|
||||||
|
json response
|
||||||
|
|
||||||
|
processCommand :: Text -> Language -> CommandResponse
|
||||||
|
processCommand "help" lang =
|
||||||
|
let helpText = getTranslation "availableCommands" lang <> "\n\n" <>
|
||||||
|
"help: " <> getTranslation "help" lang <> "\n" <>
|
||||||
|
"about: " <> getTranslation "about" lang <> "\n" <>
|
||||||
|
"skills: " <> getTranslation "skills" lang <> "\n" <>
|
||||||
|
"projects: " <> getTranslation "projects" lang <> "\n" <>
|
||||||
|
"contact: " <> getTranslation "contact" lang <> "\n" <>
|
||||||
|
"clear: " <> getTranslation "clear" lang <> "\n" <>
|
||||||
|
"language: " <> getTranslation "language" lang
|
||||||
|
in CommandResponse helpText False
|
||||||
|
|
||||||
|
processCommand "about" lang = CommandResponse (getCommandResponse "about" lang) False
|
||||||
|
processCommand "skills" lang = CommandResponse (getCommandResponse "skills" lang) False
|
||||||
|
processCommand "projects" lang = CommandResponse (getCommandResponse "projects" lang) True
|
||||||
|
processCommand "contact" lang = CommandResponse (getCommandResponse "contact" lang) True
|
||||||
|
|
||||||
|
processCommand cmd lang =
|
||||||
|
let errorMsg = getTranslation "commandNotRecognized" lang
|
||||||
|
errorMsgWithCmd = TL.replace "{0}" cmd errorMsg
|
||||||
|
in CommandResponse errorMsgWithCmd False
|
||||||
|
|
||||||
|
getTranslation :: TranslationKey -> Language -> Text
|
||||||
|
getTranslation key lang =
|
||||||
|
case lang of
|
||||||
|
French -> Map.findWithDefault "Translation missing" key frenchTranslations
|
||||||
|
English -> Map.findWithDefault "Translation missing" key englishTranslations
|
||||||
|
|
||||||
|
getCommandResponse :: Text -> Language -> Text
|
||||||
|
getCommandResponse cmd lang =
|
||||||
|
case lang of
|
||||||
|
French -> Map.findWithDefault "Response missing" cmd frenchResponses
|
||||||
|
English -> Map.findWithDefault "Response missing" cmd englishResponses
|
||||||
|
|
||||||
|
frenchTranslations :: Translation
|
||||||
|
frenchTranslations = Map.fromList
|
||||||
|
[ ("welcome", "Bienvenue dans le Portfolio de Thomas Brasdefer.\nTapez 'help' pour voir la liste des commandes disponibles. Type 'language' to switch to English.")
|
||||||
|
, ("help", "Affiche la liste des commandes disponibles")
|
||||||
|
, ("about", "À propos de moi")
|
||||||
|
, ("skills", "Mes compétences")
|
||||||
|
, ("projects", "Mes projets")
|
||||||
|
, ("contact", "Mes coordonnées")
|
||||||
|
, ("clear", "Efface l'écran")
|
||||||
|
, ("language", "Change la langue (fr/en)")
|
||||||
|
, ("languageChanged", "Langue changée en français")
|
||||||
|
, ("commandNotRecognized", "Commande non reconnue: {0}. Tapez 'help' pour voir la liste des commandes.")
|
||||||
|
, ("availableCommands", "Commandes disponibles:")
|
||||||
|
]
|
||||||
|
|
||||||
|
englishTranslations :: Translation
|
||||||
|
englishTranslations = Map.fromList
|
||||||
|
[ ("welcome", "Welcome to Thomas Brasdefer's Portfolio.\nType 'help' to see the list of available commands. Tapez 'language' pour passer en français.")
|
||||||
|
, ("help", "Display the list of available commands")
|
||||||
|
, ("about", "About me")
|
||||||
|
, ("skills", "My skills")
|
||||||
|
, ("projects", "My projects")
|
||||||
|
, ("contact", "My contact information")
|
||||||
|
, ("clear", "Clear the screen")
|
||||||
|
, ("language", "Change language (fr/en)")
|
||||||
|
, ("languageChanged", "Language changed to English")
|
||||||
|
, ("commandNotRecognized", "Command not recognized: {0}. Type 'help' to see the list of commands.")
|
||||||
|
, ("availableCommands", "Available commands:")
|
||||||
|
]
|
||||||
|
|
||||||
|
frenchResponses :: Map Text Text
|
||||||
|
frenchResponses = Map.fromList
|
||||||
|
[ ("about", "Actuellement élève ingénieur en informatique, je suis particulièrement intéressé par le développement bas niveau, la programmation logique, la cybersécurité, la cryptographie et ,dans une certaine mesure, l'intelligence artificielle. Je suis plutôt sensible aux questions de l'open source et du logiciel libre.\n\nJe cultive également un certain intérêt pour la philosophie « classique », plus particulièrement la philosophie aristotélicienne et scolastique.\nJe suis aussi assez friand de l'histoire de France lors du XIIe siècle (la « Renaissance du Moyen Âge ») et plus largement lors du reigne de la dynastie des Capétiens direct.\nMais je dois admettre que, les années n'aidant pas, j'ai de moins en moins de temps à consacrer à ces sujets.")
|
||||||
|
, ("skills", "Langages:\n- C\n- Java\n- Python\n- Prolog\n- HTML/CSS/Javascript/PHP\n\nFrameworks:\n- Flask\n- Spring Boot\n\nOutils:\n- Git\n- Docker\n\n Systèmes:\n- Linux (Debian/Ubuntu/Arch)\n- Windows")
|
||||||
|
, ("projects", "1. <a href='https://gitea.hexasec.io/tombdf/Portfolio' target='_blank'>Ce portfolio</a> (HTML, CSS, JavaScript)\n2. <a href='' target='_blank'>Générateur de documentation Prolog</a> (Prolog)\n3. <a href='https://gitea.hexasec.io/tombdf/ComGen.git' target='_blank'>Générateur de commentaire de documentation</a> (Python)")
|
||||||
|
, ("contact", "Email: <a href='mailto:thomas@hexasec.io'>thomas@hexasec.io</a>\nLinkedIn: <a href='https://www.linkedin.com/in/thomas-brasdefer-2ab818275' target='_blank'>Thomas Brasdefer</a>\nGitea: <a href='https://gitea.hexasec.io/tombdf' target='_blank'>tombdf</a>")
|
||||||
|
]
|
||||||
|
|
||||||
|
englishResponses :: Map Text Text
|
||||||
|
englishResponses = Map.fromList
|
||||||
|
[ ("about", "Currently a student in computer engineering, I'm particularly interested in low-level programming, logic programming, cybersecurity, cryptography and, to a certain extent, artificial intelligence. I'm very interested in open source and free software.\n\nI also cultivate a certain interest in \"classical\" philosophy, particularly Aristotelian and Scholastic philosophy.\nI'm also quite fond of French history during the 12th century (the \"Renaissance of the Middle Ages\") and more broadly during the reign of the direct Capetian dynasty.\nBut I have to admit that, as the years go by, I have less and less time to devote to these subjects.")
|
||||||
|
, ("skills", "Languages:\n- C\n- Java\n- Python\n- Prolog\n- HTML/CSS/Javascript/PHP\n\nFrameworks:\n- Flask\n- Spring Boot\n\nTools:\n- Git\n- Docker\n\n OS:\n- Linux (Debian/Ubuntu/Arch)\n- Windows")
|
||||||
|
, ("projects", "1. <a href='https://gitea.hexasec.io/tombdf/Portfolio' target='_blank'>This portfolio</a> (HTML, CSS, JavaScript)\n2. <a href='' target='_blank'>Prolog documentation generator</a> (Prolog)\n3. <a href='https://gitea.hexasec.io/tombdf/ComGen.git' target='_blank'>Comments generator</a> (Python)")
|
||||||
|
, ("contact", "Email: <a href='mailto:thomas@hexasec.io'>thomas@hexasec.io</a>\nLinkedIn: <a href='https://www.linkedin.com/in/thomas-brasdefer-2ab818275' target='_blank'>Thomas Brasdefer</a>\nGitea: <a href='https://gitea.hexasec.io/tombdf' target='_blank'>tombdf</a>")
|
||||||
|
]
|
||||||
40
app/Templates.hs
Normal file
40
app/Templates.hs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Templates where
|
||||||
|
|
||||||
|
import Data.Text.Lazy (Text)
|
||||||
|
import qualified Data.Text.Lazy as TL
|
||||||
|
import Types
|
||||||
|
|
||||||
|
renderIndex :: Language -> Text
|
||||||
|
renderIndex lang = TL.concat
|
||||||
|
[ "<!DOCTYPE html>\n"
|
||||||
|
, "<html lang=\"", languageCode lang, "\">\n"
|
||||||
|
, " <head>\n"
|
||||||
|
, " <meta charset=\"UTF-8\">\n"
|
||||||
|
, " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"
|
||||||
|
, " <meta name=\"robots\" content=\"noindex, nofollow\">\n"
|
||||||
|
, " <title>Portfolio</title>\n"
|
||||||
|
, " <link rel=\"stylesheet\" href=\"/css/style.css\">\n"
|
||||||
|
, " </head>\n"
|
||||||
|
, " <body>\n"
|
||||||
|
, " <div id=\"terminal\">\n"
|
||||||
|
, " <div id=\"welcome-text\"></div>\n"
|
||||||
|
, " <div id=\"input-line\" style=\"display: none;\">\n"
|
||||||
|
, " <span id=\"prompt\">$</span>\n"
|
||||||
|
, " <input type=\"text\" id=\"user-input\" autofocus>\n"
|
||||||
|
, " </div>\n"
|
||||||
|
, " </div>\n"
|
||||||
|
, " <script src=\"/js/terminal.js\"></script>\n"
|
||||||
|
, " <script>\n"
|
||||||
|
, " window.onload = function() {\n"
|
||||||
|
, " initTerminal('", languageCode lang, "');\n"
|
||||||
|
, " }\n"
|
||||||
|
, " </script>\n"
|
||||||
|
, " </body>\n"
|
||||||
|
, "</html>"
|
||||||
|
]
|
||||||
|
|
||||||
|
languageCode :: Language -> Text
|
||||||
|
languageCode French = "fr"
|
||||||
|
languageCode English = "en"
|
||||||
27
app/Types.hs
Normal file
27
app/Types.hs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Types where
|
||||||
|
|
||||||
|
import Data.Text.Lazy (Text)
|
||||||
|
import Data.Map.Strict (Map)
|
||||||
|
import qualified Data.Map.Strict as Map
|
||||||
|
import Data.Aeson (ToJSON(..), object, (.=))
|
||||||
|
|
||||||
|
data Language = French | English
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
|
type TranslationKey = Text
|
||||||
|
type TranslationValue = Text
|
||||||
|
type Translation = Map TranslationKey TranslationValue
|
||||||
|
|
||||||
|
data CommandResponse = CommandResponse
|
||||||
|
{ responseText :: Text
|
||||||
|
, responseHtml :: Bool
|
||||||
|
} deriving (Show)
|
||||||
|
|
||||||
|
-- Instance ToJSON pour permettre la sérialisation JSON
|
||||||
|
instance ToJSON CommandResponse where
|
||||||
|
toJSON (CommandResponse text html) =
|
||||||
|
object [ "responseText" .= text
|
||||||
|
, "responseHtml" .= html
|
||||||
|
]
|
||||||
137
index.html
137
index.html
@ -1,137 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="robots" content="noindex, nofollow">
|
|
||||||
<title>Portfolio</title>
|
|
||||||
<link style="text/css" rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="terminal">
|
|
||||||
<div id="welcome-text"></div>
|
|
||||||
<div id="input-line" style="display: none;">
|
|
||||||
<span id="prompt">$</span>
|
|
||||||
<input type="text" id="user-input" autofocus>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const terminal = document.getElementById('terminal');
|
|
||||||
const userInput = document.getElementById('user-input');
|
|
||||||
const welcomeText = document.getElementById('welcome-text');
|
|
||||||
const inputLine = document.getElementById('input-line');
|
|
||||||
|
|
||||||
let currentLanguage = 'fr';
|
|
||||||
|
|
||||||
const translations = {
|
|
||||||
fr: {
|
|
||||||
welcome: "Bienvenue dans le Portfolio de Thomas Brasdefer.\nTapez 'help' pour voir la liste des commandes disponibles. Type 'language' to switch to English.",
|
|
||||||
help: "Affiche la liste des commandes disponibles",
|
|
||||||
about: "À propos de moi",
|
|
||||||
skills: "Mes compétences",
|
|
||||||
projects: "Mes projets",
|
|
||||||
contact: "Mes coordonnées",
|
|
||||||
clear: "Efface l'écran",
|
|
||||||
language: "Change la langue (fr/en)",
|
|
||||||
languageChanged: "Langue changée en français",
|
|
||||||
commandNotRecognized: "Commande non reconnue: {0}. Tapez 'help' pour voir la liste des commandes.",
|
|
||||||
availableCommands: "Commandes disponibles:"
|
|
||||||
},
|
|
||||||
en: {
|
|
||||||
welcome: "Welcome to Thomas Brasdefer's Portfolio.\nType 'help' to see the list of available commands. Tapez 'language' pour passer en français.",
|
|
||||||
help: "Display the list of available commands",
|
|
||||||
about: "About me",
|
|
||||||
skills: "My skills",
|
|
||||||
projects: "My projects",
|
|
||||||
contact: "My contact information",
|
|
||||||
clear: "Clear the screen",
|
|
||||||
language: "Change language (fr/en)",
|
|
||||||
languageChanged: "Language changed to English",
|
|
||||||
commandNotRecognized: "Command not recognized: {0}. Type 'help' to see the list of commands.",
|
|
||||||
availableCommands: "Available commands:"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const commandResponses = {
|
|
||||||
about: {
|
|
||||||
fr: "Actuellement élève ingénieur en informatique, je suis particulièrement intéressé par le développement bas niveau, la programmation logique, la cybersécurité, la cryptographie et ,dans une certaine mesure, l'intelligence artificielle. Je suis plutôt sensible aux questions de l'open source et du logiciel libre.\n\nJe cultive également un certain intérêt pour la philosophie « classique », plus particulièrement la philosophie aristotélicienne et scolastique.\nJe suis aussi assez friand de l'histoire de France lors du XIIe siècle (la « Renaissance du Moyen Âge ») et plus largement lors du reigne de la dynastie des Capétiens direct.\nMais je dois admettre que, les années n'aidant pas, j'ai de moins en moins de temps à consacrer à ces sujets.",
|
|
||||||
en: "Currently a student in computer engineering, I'm particularly interested in low-level programming, logic programming, cybersecurity, cryptography and, to a certain extent, artificial intelligence. I'm very interested in open source and free software.\n\nI also cultivate a certain interest in “classical” philosophy, particularly Aristotelian and Scholastic philosophy.\nI'm also quite fond of French history during the 12th century (the “Renaissance of the Middle Ages”) and more broadly during the reign of the direct Capetian dynasty.\nBut I have to admit that, as the years go by, I have less and less time to devote to these subjects."
|
|
||||||
},
|
|
||||||
skills: {
|
|
||||||
fr: "Langages:\n- C\n- Java\n- Python\n- Prolog\n- HTML/CSS/Javascript/PHP\n\nFrameworks:\n- Flask\n- Spring Boot\n\nOutils:\n- Git\n- Docker\n\n Systèmes:\n- Linux (Debian/Ubuntu/Arch)\n- Windows",
|
|
||||||
en: "Languages:\n- C\n- Java\n- Python\n- Prolog\n- HTML/CSS/Javascript/PHP\n\nFrameworks:\n- Flask\n- Spring Boot\n\nTools:\n- Git\n- Docker\n\n OS:\n- Linux (Debian/Ubuntu/Arch)\n- Windows"
|
|
||||||
},
|
|
||||||
projects: {
|
|
||||||
fr: "1. <a href='https://gitea.hexasec.io/tombdf/Portfolio' target='_blank'>Ce portfolio</a> (HTML, CSS, JavaScript)\n2. <a href='' target='_blank'>Générateur de documentation Prolog</a> (Prolog)\n3. <a href='https://gitea.hexasec.io/tombdf/ComGen.git' target='_blank'>Générateur de commentaire de documentation</a> (Python)",
|
|
||||||
en: "1. <a href='https://gitea.hexasec.io/tombdf/Portfolio' target='_blank'>This portfolio</a> (HTML, CSS, JavaScript)\n2. <a href='' target='_blank'>Prolog documentation generator</a> (Prolog)\n3. <a href='https://gitea.hexasec.io/tombdf/ComGen.git' target='_blank'>Comments generator</a> (Python)"
|
|
||||||
},
|
|
||||||
contact: {
|
|
||||||
fr: "Email: <a href='mailto:thomas@hexasec.io'>thomas@hexasec.io</a>\nLinkedIn: <a href='https://www.linkedin.com/in/thomas-brasdefer-2ab818275' target='_blank'>Thomas Brasdefer</a>\nGitea: <a href='https://gitea.hexasec.io/tombdf' target='_blank'>tombdf</a>",
|
|
||||||
en: "Email: <a href='mailto:thomas@hexasec.io'>thomas@hexasec.io</a>\nLinkedIn: <a href='https://www.linkedin.com/in/thomas-brasdefer-2ab818275' target='_blank'>Thomas Brasdefer</a>\nGitea: <a href='https://gitea.hexasec.io/tombdf' target='_blank'>tombdf</a>"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function typeText(text, element, speed = 50) {
|
|
||||||
let i = 0;
|
|
||||||
element.innerHTML = '';
|
|
||||||
function type() {
|
|
||||||
if (i < text.length) {
|
|
||||||
element.innerHTML += text.charAt(i);
|
|
||||||
i++;
|
|
||||||
setTimeout(type, speed);
|
|
||||||
} else {
|
|
||||||
inputLine.style.display = 'flex';
|
|
||||||
userInput.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onload = function() {
|
|
||||||
typeText(translations[currentLanguage].welcome, welcomeText);
|
|
||||||
}
|
|
||||||
|
|
||||||
userInput.addEventListener('keydown', function(event) {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
const command = userInput.value.trim().toLowerCase();
|
|
||||||
addToTerminal(`$ ${command}`);
|
|
||||||
processCommand(command);
|
|
||||||
userInput.value = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function addToTerminal(text) {
|
|
||||||
const output = document.createElement('div');
|
|
||||||
output.classList.add('output');
|
|
||||||
output.innerHTML = text;
|
|
||||||
terminal.insertBefore(output, inputLine);
|
|
||||||
terminal.scrollTop = terminal.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function processCommand(command) {
|
|
||||||
if (command === 'help') {
|
|
||||||
let helpText = translations[currentLanguage].availableCommands + '\n\n';
|
|
||||||
for (let cmd in translations[currentLanguage]) {
|
|
||||||
if (cmd !== 'welcome' && cmd !== 'languageChanged' && cmd !== 'commandNotRecognized' && cmd !== 'availableCommands') {
|
|
||||||
helpText += `${cmd}: ${translations[currentLanguage][cmd]}\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addToTerminal(helpText);
|
|
||||||
} else if (command === 'clear') {
|
|
||||||
const outputs = document.querySelectorAll('.output');
|
|
||||||
outputs.forEach(output => output.remove());
|
|
||||||
welcomeText.innerHTML = '';
|
|
||||||
} else if (command === 'language') {
|
|
||||||
currentLanguage = currentLanguage === 'fr' ? 'en' : 'fr';
|
|
||||||
addToTerminal(translations[currentLanguage].languageChanged);
|
|
||||||
} else if (commandResponses.hasOwnProperty(command)) {
|
|
||||||
addToTerminal(commandResponses[command][currentLanguage]);
|
|
||||||
} else {
|
|
||||||
addToTerminal(translations[currentLanguage].commandNotRecognized.replace('{0}', command));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
37
package.yaml
Normal file
37
package.yaml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: portfolio
|
||||||
|
version: 0.1.0.0
|
||||||
|
github: "yourusername/portfolio"
|
||||||
|
license: BSD3
|
||||||
|
author: "Thomas Brasdefer"
|
||||||
|
maintainer: "thomas@hexasec.io"
|
||||||
|
copyright: "2025 Thomas Brasdefer"
|
||||||
|
|
||||||
|
description: Portfolio website using Haskell and Scotty
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- base >= 4.7 && < 5
|
||||||
|
- scotty
|
||||||
|
- wai
|
||||||
|
- wai-extra
|
||||||
|
- text
|
||||||
|
- bytestring
|
||||||
|
- containers
|
||||||
|
- transformers
|
||||||
|
- aeson
|
||||||
|
- warp
|
||||||
|
- http-types
|
||||||
|
- wai-middleware-static
|
||||||
|
|
||||||
|
library:
|
||||||
|
source-dirs: app
|
||||||
|
|
||||||
|
executables:
|
||||||
|
portfolio-exe:
|
||||||
|
main: Main.hs
|
||||||
|
source-dirs: app
|
||||||
|
ghc-options:
|
||||||
|
- -threaded
|
||||||
|
- -rtsopts
|
||||||
|
- -with-rtsopts=-N
|
||||||
|
dependencies:
|
||||||
|
- portfolio
|
||||||
6
stack.yaml
Normal file
6
stack.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
resolver: lts-21.22
|
||||||
|
packages:
|
||||||
|
- .
|
||||||
|
extra-deps: []
|
||||||
|
flags: {}
|
||||||
|
extra-package-dbs: []
|
||||||
12
stack.yaml.lock
Normal file
12
stack.yaml.lock
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# This file was autogenerated by Stack.
|
||||||
|
# You should not edit this file by hand.
|
||||||
|
# For more information, please see the documentation at:
|
||||||
|
# https://docs.haskellstack.org/en/stable/topics/lock_files
|
||||||
|
|
||||||
|
packages: []
|
||||||
|
snapshots:
|
||||||
|
- completed:
|
||||||
|
sha256: afd5ba64ab602cabc2d3942d3d7e7dd6311bc626dcb415b901eaf576cb62f0ea
|
||||||
|
size: 640060
|
||||||
|
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/21/22.yaml
|
||||||
|
original: lts-21.22
|
||||||
80
static/js/terminal.js
Normal file
80
static/js/terminal.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
let currentLanguage = 'fr';
|
||||||
|
const terminal = document.getElementById('terminal');
|
||||||
|
const userInput = document.getElementById('user-input');
|
||||||
|
const welcomeText = document.getElementById('welcome-text');
|
||||||
|
const inputLine = document.getElementById('input-line');
|
||||||
|
|
||||||
|
async function initTerminal(lang) {
|
||||||
|
currentLanguage = lang;
|
||||||
|
const response = await fetch(`/api/command/help?lang=${currentLanguage}`);
|
||||||
|
const data = await response.json();
|
||||||
|
const welcomeMsg = currentLanguage === 'fr'
|
||||||
|
? "Bienvenue dans le Portfolio de Thomas Brasdefer.\nTapez 'help' pour voir la liste des commandes disponibles. Type 'language' to switch to English."
|
||||||
|
: "Welcome to Thomas Brasdefer's Portfolio.\nType 'help' to see the list of available commands. Tapez 'language' pour passer en français.";
|
||||||
|
|
||||||
|
typeText(welcomeMsg, welcomeText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeText(text, element, speed = 50) {
|
||||||
|
let i = 0;
|
||||||
|
element.innerHTML = '';
|
||||||
|
function type() {
|
||||||
|
if (i < text.length) {
|
||||||
|
element.innerHTML += text.charAt(i);
|
||||||
|
i++;
|
||||||
|
setTimeout(type, speed);
|
||||||
|
} else {
|
||||||
|
inputLine.style.display = 'flex';
|
||||||
|
userInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type();
|
||||||
|
}
|
||||||
|
|
||||||
|
userInput.addEventListener('keydown', async function(event) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
const command = userInput.value.trim().toLowerCase();
|
||||||
|
addToTerminal(`$ ${command}`);
|
||||||
|
|
||||||
|
if (command === 'clear') {
|
||||||
|
clearTerminal();
|
||||||
|
} else if (command === 'language') {
|
||||||
|
currentLanguage = currentLanguage === 'fr' ? 'en' : 'fr';
|
||||||
|
const response = await fetch(`/api/language/${currentLanguage}`);
|
||||||
|
const data = await response.json();
|
||||||
|
addToTerminal(data.responseText);
|
||||||
|
} else {
|
||||||
|
const response = await fetch(`/api/command/${command}?lang=${currentLanguage}`);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.responseHtml) {
|
||||||
|
addToTerminalHtml(data.responseText);
|
||||||
|
} else {
|
||||||
|
addToTerminal(data.responseText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userInput.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function addToTerminal(text) {
|
||||||
|
const output = document.createElement('div');
|
||||||
|
output.classList.add('output');
|
||||||
|
output.textContent = text;
|
||||||
|
terminal.insertBefore(output, inputLine);
|
||||||
|
terminal.scrollTop = terminal.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addToTerminalHtml(html) {
|
||||||
|
const output = document.createElement('div');
|
||||||
|
output.classList.add('output');
|
||||||
|
output.innerHTML = html;
|
||||||
|
terminal.insertBefore(output, inputLine);
|
||||||
|
terminal.scrollTop = terminal.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTerminal() {
|
||||||
|
const outputs = document.querySelectorAll('.output');
|
||||||
|
outputs.forEach(output => output.remove());
|
||||||
|
welcomeText.innerHTML = '';
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user