Merge pull request 'Sveltekit migration with redesign' (#11) from development into master
Reviewed-on: #11
This commit is contained in:
commit
d4480c49d7
177 changed files with 8588 additions and 5657 deletions
|
|
@ -1,15 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.toml]
|
||||
max_line_length = 100
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
27
.forgejo/workflows/deploy-prod.yml
Normal file
27
.forgejo/workflows/deploy-prod.yml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
name: Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: docker
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.SSH_KEY }}" > ./deploy.key
|
||||
chmod 600 ./deploy.key
|
||||
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||
|
||||
- name: Install PM2
|
||||
run: npm i -g pm2
|
||||
|
||||
- name: Deploy
|
||||
run: pm2 deploy ecosystem.config.cjs production
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
name: Deploy Site
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: docker
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Make and rsync
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y make rsync
|
||||
|
||||
- name: Add Deploy Key to SSH
|
||||
run: |
|
||||
mkdir ~/.ssh
|
||||
echo "${{ secrets.SSH_KEY }}" >> ~/.ssh/id_ed25519_deployer
|
||||
chmod 400 ~/.ssh/id_ed25519_deployer
|
||||
echo -e "Host deployer\n\tUser deployer\n\tHostname 45.76.5.44\n\tIdentityFile ~/.ssh/id_ed25519_deployer\n\tStrictHostKeyChecking No" >> ~/.ssh/config
|
||||
|
||||
- name: Install Hugo
|
||||
run: |
|
||||
wget https://github.com/gohugoio/hugo/releases/download/v0.145.0/hugo_0.145.0_linux-amd64.deb
|
||||
dpkg -i hugo_0.145.0_linux-amd64.deb
|
||||
|
||||
- name: Run Make Deploy
|
||||
run: |
|
||||
make deploy
|
||||
38
.gitignore
vendored
38
.gitignore
vendored
|
|
@ -1,19 +1,25 @@
|
|||
src/articles/*
|
||||
node_modules
|
||||
|
||||
# Generated files by hugo
|
||||
/public/
|
||||
/resources/_gen/
|
||||
/assets/jsconfig.json
|
||||
hugo_stats.json
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# Executable may be added to repository
|
||||
hugo.exe
|
||||
hugo.darwin
|
||||
hugo.linux
|
||||
|
||||
# Temporary lock file while building
|
||||
/.hugo_build.lock
|
||||
|
||||
# MacOS
|
||||
static/.DS_Store
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
deploy.key
|
||||
|
|
|
|||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
||||
22
.prettierignore
Normal file
22
.prettierignore
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Ignore node_modules
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
.build/
|
||||
.svelte-kit/
|
||||
dist/
|
||||
|
||||
# Ignore lock files
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
# Ignore environment files
|
||||
.env
|
||||
.env.*.local
|
||||
|
||||
# VSCode settings
|
||||
.vscode/
|
||||
|
||||
# Ignore output from lint or test tools
|
||||
coverage/
|
||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 80,
|
||||
"semi": true,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
29
Makefile
29
Makefile
|
|
@ -1,29 +0,0 @@
|
|||
#!/usr/bin/make -f
|
||||
|
||||
.PHONY: help init build deploy clean serve
|
||||
|
||||
BLOG_REMOTE=deployer:/home/deployer/leomurca.xyz
|
||||
|
||||
help:
|
||||
$(info make init|deploy|build|clean|serve)
|
||||
|
||||
init:
|
||||
echo "Making $@"; \
|
||||
|
||||
build: clean
|
||||
echo "Making $@"
|
||||
hugo --minify
|
||||
|
||||
deploy: build
|
||||
echo "Making $@"
|
||||
rsync -rLtvz public/ $(BLOG_REMOTE)
|
||||
|
||||
clean:
|
||||
echo "Making $@"
|
||||
rm -rf public/
|
||||
|
||||
prod:
|
||||
python3 -m http.server --directory public
|
||||
|
||||
dev:
|
||||
hugo server
|
||||
18
README.md
18
README.md
|
|
@ -2,5 +2,21 @@
|
|||
|
||||

|
||||
|
||||
My personal website source code. You can access it [here](https://leomurca.xyz).
|
||||
Personal site source (SvelteKit). Live site: [leomurca.xyz](https://leomurca.xyz).
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Set `PUBLIC_IMAGE_BASE_URL` in `.env` (Cloudinary `…/image/upload` base URL). See `.env.example`.
|
||||
|
||||
## Scripts
|
||||
|
||||
| Command | Description |
|
||||
| -------------- | ------------------------ |
|
||||
| `npm run dev` | Vite dev server |
|
||||
| `npm run build`| Production build (`build/`) |
|
||||
| `npm run check`| Typecheck + Svelte check |
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
---
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
featured_image: "/img/{{ .Name }}.webp"
|
||||
draft: true
|
||||
---
|
||||
72
config.toml
72
config.toml
|
|
@ -1,72 +0,0 @@
|
|||
baseURL = 'https://leomurca.xyz'
|
||||
title = 'Leonardo Murça'
|
||||
defaultContentLanguage='en'
|
||||
|
||||
enableRobotsTXT = true
|
||||
|
||||
# Code Highlight
|
||||
pygmentsstyle = "gruvbox"
|
||||
pygmentscodefences = true
|
||||
pygmentscodefencesguesssyntax = true
|
||||
|
||||
# RSS Feed
|
||||
rssLimit = 20
|
||||
copyright = "copyright"
|
||||
|
||||
[params]
|
||||
description = "home-page-description"
|
||||
featured_image = "/img/avatar.webp"
|
||||
[params.author]
|
||||
name = "Leonardo Murça"
|
||||
email = "leo@leomurca.xyz"
|
||||
|
||||
[languages]
|
||||
[languages.en]
|
||||
languageCode = 'en'
|
||||
contentDir = 'content/en'
|
||||
|
||||
[languages.en.menu]
|
||||
[[languages.en.menu.main]]
|
||||
name = "Home"
|
||||
url = "/"
|
||||
weight = 1
|
||||
|
||||
[[languages.en.menu.main]]
|
||||
name = "All posts"
|
||||
url = "/posts"
|
||||
weight = 2
|
||||
|
||||
[[languages.en.menu.main]]
|
||||
name = "Contact me"
|
||||
url = "/contact"
|
||||
weight = 3
|
||||
|
||||
[[languages.en.menu.main]]
|
||||
name = "Donate"
|
||||
url = "/donate"
|
||||
weight = 4
|
||||
|
||||
[languages.pt-br]
|
||||
languageCode = 'pt-br'
|
||||
contentDir = 'content/pt-br'
|
||||
|
||||
[languages.pt-br.menu]
|
||||
[[languages.pt-br.menu.main]]
|
||||
name = "Página Inicial"
|
||||
url = "/pt-br"
|
||||
weight = 1
|
||||
|
||||
[[languages.pt-br.menu.main]]
|
||||
name = "Publicações"
|
||||
url = "/pt-br/posts"
|
||||
weight = 2
|
||||
|
||||
[[languages.pt-br.menu.main]]
|
||||
name = "Contato"
|
||||
url = "/pt-br/contact"
|
||||
weight = 3
|
||||
|
||||
[[languages.pt-br.menu.main]]
|
||||
name = "Doe"
|
||||
url = "/pt-br/donate"
|
||||
weight = 4
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
title: "Contact Me"
|
||||
date: 2022-10-15
|
||||
description: "If you have any questions, topics to talk about or paid work to offer, feel free to reach me out."
|
||||
featured_image: "/img/avatar.webp"
|
||||
draft: false
|
||||
---
|
||||
|
||||
If you have any questions, topics to talk about or paid work to offer, feel free to reach me out.
|
||||
|
||||
- E-mail: leo@leomurca.xyz
|
||||
- LinkedIn: https://linkedin.com/in/leonardoamurca
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
---
|
||||
title: "Donate"
|
||||
date: 2022-11-16T19:08:42-03:00
|
||||
description: "If my work or knowledge has helped you in any way, please consider supporting me financially."
|
||||
featured_image: "/img/avatar.webp"
|
||||
draft: false
|
||||
---
|
||||
|
||||
If my work or knowledge has helped you in any way, please consider supporting me financially.
|
||||
|
||||
- {{< icon src="bat.svg" alt="Brave Attention Token Icon" >}} **BAT (Brave Attention Token)**: Send me a tip through [Brave Rewards](https://support.brave.com/hc/en-us/articles/360021123971-How-do-I-tip-websites-and-Content-Creators-in-Brave-Rewards-#:~:text=In%20the%20tipping%20banner%20%2C%20the,tip%20to%20complete%20the%20transaction.).
|
||||
|
||||
- {{< icon src="bitcoin.svg" alt="Bitcon Icon" >}} **Bitcoin**: `bc1qpc4lpyr6stxrrg3u0k4clp4crlt6z4j6q845rq`.
|
||||
- {{< icon src="monero.svg" alt="Monero Icon" >}} **Monero**: `8A9iyTskiBh6f6GDUwnUJaYhAW13gNjDYaZYJBftX434D3XLrcGBko4a8kC4pLSfiuJAoSJ7e8rwP8W4StsVypftCp6FGwm`.
|
||||
|
||||
Feel free to also send me a message at **leo@leomurca.xyz** after donating.
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
---
|
||||
title: "Best Place to Store Your Master Password"
|
||||
date: 2023-04-16T12:41:54-03:00
|
||||
featured_image: "/img/best-place-to-store-your-master-password.webp"
|
||||
draft: true
|
||||
---
|
||||
|
||||
# TL;DR
|
||||
|
||||
|
||||
# Motivation
|
||||
|
||||
Personally, I have always been a big fan of [Password Managers](https://en.wikipedia.org/wiki/Password_manager). However, I have had some doubts on how to store it. What place is the most secure? Also, is it **convenient**? With that in mind, I came to a strategy that have been working good for me. Let's check out.
|
||||
|
||||
# The solution
|
||||
|
||||
## pass
|
||||
|
||||
## KeePass
|
||||
|
||||
## Cloud File storage (Nextcloud)
|
||||
|
||||
# The state of the art
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
---
|
||||
title: "Best Use Cases for WebViews in Android Development"
|
||||
date: 2023-02-14T19:58:00-03:00
|
||||
featured_image: "/img/best-use-cases-for-webViews-in-android-development.webp"
|
||||
draft: true
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
WebViews in native Android development can be used for a variety of purposes, but some of the best use cases are:
|
||||
|
||||
1. Provide information in your app that you might need to update, such as end-user agreement or a user guide;
|
||||
|
||||
2. Display web pages such as news articles, social media feeds, and web-based applications, within your app.
|
||||
|
||||
3. Authentication: If your app requires authentication with an external service, you can use WebViews to display the login page and handle the authentication flow.
|
||||
|
||||
(INVALID) 4. Payment gateways: You can use WebViews to display payment gateways within your app. This allows you to accept payments from within your app without the need to redirect users to a separate website.
|
||||
|
||||
4. HTML5 games: If your app includes HTML5 games, you can use WebViews to display them within the app.
|
||||
|
||||
5. Advertising: You can use WebViews to display advertisements within your app. This allows you to monetize your app by displaying ads without the need to leave the app.
|
||||
|
||||
6. Custom UI elements: You can use WebViews to create custom UI elements within your app, such as custom buttons or menus.
|
||||
|
||||
7. Chatbots: You can use WebViews to display chatbots within your app. This allows users to interact with chatbots without leaving the app.
|
||||
|
||||
8. Implement a MVP: If you want to validate an idea but you don't have enough android engineers to implementa all natively, just port your existing web app to Android webview.
|
||||
|
||||
Overall, WebViews can be a useful tool for integrating web-based content and services into your native Android app.
|
||||
|
|
@ -1,575 +0,0 @@
|
|||
---
|
||||
title: "Demystifying ViewModel Testing: Strategies for Crafting Test-Friendly ViewModels"
|
||||
date: 2024-03-25T10:54:21-03:00
|
||||
description: "Learn the best practices for testing your ViewModels."
|
||||
featured_image: "/img/demystifying-viewmodel-testing/featured_image.webp"
|
||||
draft: false
|
||||
---
|
||||
|
||||

|
||||
|
||||
## Introduction
|
||||
|
||||
Have you ever been struggling when writing unit tests for your ViewModel? Difficulty
|
||||
when writing tests is a BIG symptom that your ViewModel is poorly written. If the mere
|
||||
thought of testing your ViewModel sends shivers down your spine or if you find yourself
|
||||
wrestling with intricate setups just to verify a simple behavior, fear not – you're not
|
||||
alone. Writing test-friendly ViewModels is a common challenge faced by many
|
||||
developers, but the good news is that it's a challenge that can be conquered.
|
||||
|
||||
In this blog post, we'll explore the reasons behind the testing struggle, unraveling the
|
||||
intricacies of ViewModel design that lead to testing headaches. More importantly, we'll
|
||||
equip you with practical insights and techniques to transform your ViewModels into
|
||||
test-friendly units, making the process of unit testing a seamless and efficient
|
||||
experience. Let's embark on a journey to banish testing woes and elevate your
|
||||
ViewModel game!
|
||||
|
||||
## What's a ViewModel?
|
||||
|
||||
First thing first, we need to define what a ViewModel is and what its existence purpose is.
|
||||
According to the [ViewModel overview](https://developer.android.com/topic/libraries/architecture/viewmodel):
|
||||
*"[...] the ViewModel class is a business logic or screen level state holder.".* In other words,
|
||||
it encapsulates related business logic and it exposes the Screen UI State.
|
||||
|
||||
However, any plain Kotlin class can be used as a StateHolder to encapsulate business
|
||||
logic and expose some Screen UI State. So why do we need ViewModels?
|
||||
|
||||
The main reason that we use a ViewModel instead of a plain Kotlin class is that
|
||||
ViewModels:
|
||||
- Survives configuration changes (lifecycle-aware). These configuration changes
|
||||
are related to a data persistence benefit from using ViewModels.
|
||||
- Has great Jetpack integration and other libraries;
|
||||
- Caches states.
|
||||
|
||||
Well, knowing the definition of a ViewModel and the reasons why we should use it, we
|
||||
must implement and maintain it very carefully. And in case your ViewModel already
|
||||
exists, pay attention to the symptoms that may indicate that it needs some care.
|
||||
|
||||
## Symptoms that indicate your ViewModel needs some care
|
||||
|
||||
|
||||
### 1. Heavy logic
|
||||
Having complex business logic or extensive data manipulation directly in the
|
||||
ViewModel can be a great indicator that your ViewModel needs some attention.
|
||||
This symptom will cause you lots of headaches testing and maintaining it. Notice
|
||||
the `UserProfileViewModel` below, for example:
|
||||
|
||||
```kotlin
|
||||
class UserProfileViewModel(
|
||||
private val userRepository: UserRepository,
|
||||
private val userLocalDataSource: UserLocalDataSource
|
||||
) : ViewModel() {
|
||||
|
||||
private val _userProfileState = MutableStateFlow<UserProfile?>(null)
|
||||
val userProfileState: StateFlow<UserProfile?> get() = _userProfileState
|
||||
|
||||
private val _loadingState = MutableStateFlow<Boolean>(false)
|
||||
val loadingState: StateFlow<Boolean> get() = _loadingState
|
||||
|
||||
init {
|
||||
// Initial loading of user profile
|
||||
loadUserProfile()
|
||||
}
|
||||
|
||||
private fun loadUserProfile() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
_loadingState.emit(true)
|
||||
// Fetching user details from a remote server
|
||||
val remoteUserDetails = userRepository.fetchUserDetails()
|
||||
// Processing and transforming user details
|
||||
val processedUserProfile = processUserProfile(remoteUserDetails)
|
||||
// Updating the local database with the processed data
|
||||
userLocalDataSource.updateUserProfile(processedUserProfile)
|
||||
_userProfileState.emit(processedUserProfile)
|
||||
} catch (e: Exception) {
|
||||
// Handle errors and update UI accordingly
|
||||
} finally {
|
||||
_loadingState.emit(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processUserProfile(userDetails: UserDetails): UserProfile {
|
||||
// Heavy processing and transformation of user details
|
||||
// ...
|
||||
return UserProfile(/* Processed user profile data */)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Large ViewModels
|
||||
A large class is a well-known [code smell](https://martinfowler.com/bliki/CodeSmell.html)
|
||||
for classes that have too many responsibilities. That's not different for ViewModels. ViewModel's only
|
||||
responsibility is to manage the data for the UI. Having overly large ViewModels
|
||||
with too many responsibilities can make code hard to understand, test, and
|
||||
maintain. Be mindful that following the [SRP](https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html)
|
||||
is fundamental for ViewModels too. See the example below with too many responsibilities for a single ViewModel:
|
||||
|
||||
|
||||
```kotlin
|
||||
class LargeViewModel(
|
||||
private val userRepository: UserRepository,
|
||||
private val taskRepository: TaskRepository,
|
||||
private val analyticsManager: AnalyticsManager,
|
||||
// ... other dependencies ...
|
||||
) : ViewModel() {
|
||||
// Properties for various data streams
|
||||
private val _userProfileState = MutableStateFlow<UserProfile?>(null)
|
||||
|
||||
val userProfileState: StateFlow<UserProfile?> get() = _userProfileState
|
||||
private val _tasksState = MutableStateFlow<List<Task>>(emptyList())
|
||||
|
||||
val tasksState: StateFlow<List<Task>> get() = _tasksState
|
||||
|
||||
// ... Other properties for different features ...
|
||||
|
||||
private val _loadingState = MutableStateFlow<Boolean>(false)
|
||||
val loadingState: StateFlow<Boolean> get() = _loadingState
|
||||
|
||||
init {
|
||||
// Initial loading of data for various features
|
||||
loadData()
|
||||
}
|
||||
|
||||
private fun loadData() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
_loadingState.emit(true)
|
||||
// Fetching user details from a remote server
|
||||
val remoteUserDetails = userRepository.fetchUserDetails()
|
||||
_userProfileState.emit(processUserProfile(remoteUserDetails))
|
||||
// Fetching and processing tasks
|
||||
val remoteTasks = taskRepository.fetchTasks()
|
||||
_tasksState.emit(processTasks(remoteTasks))
|
||||
// ... Load data for other features ...
|
||||
// Sending analytics events
|
||||
analyticsManager.logEvent("DataLoaded")
|
||||
} catch (e: Exception) {
|
||||
// Handle errors and update UI accordingly
|
||||
} finally {
|
||||
_loadingState.emit(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ... Other methods for processing data, handling user interactions, etc. ...
|
||||
private suspend fun processUserProfile(userDetails: UserDetails): UserProfile {
|
||||
// Processing user details
|
||||
// ...
|
||||
return UserProfile(/* Processed user profile data */)
|
||||
}
|
||||
|
||||
private suspend fun processTasks(tasks: List<Task>): List<Task> {
|
||||
// Processing tasks
|
||||
// ...
|
||||
return tasks
|
||||
}
|
||||
// ... Other methods for different features ...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 3. Direct Android Framework References
|
||||
Avoid direct references to Android framework components like Context or View
|
||||
within the ViewModel. This makes the ViewModel less testable and can lead to
|
||||
memory leaks. If something needs a Context in the ViewModel, you should
|
||||
strongly evaluate if that is in the right layer. ViewModels should be designed to
|
||||
be testable in isolation from the Android framework. Don't get your ViewModel
|
||||
too tight to a framework, make it as agnostic as possible. A common example is
|
||||
when we need to access the device Location. See `LocationViewModel` below:
|
||||
|
||||
```kotlin
|
||||
class LocationViewModel(private val context: Context) : ViewModel() {
|
||||
|
||||
private val _locationState = MutableStateFlow<Location?>(null)
|
||||
val locationState: StateFlow<Location?> get() = _locationState
|
||||
|
||||
private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
|
||||
init {
|
||||
// Start listening for location updates
|
||||
startLocationUpdates()
|
||||
}
|
||||
|
||||
private fun startLocationUpdates() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
1000,
|
||||
10,
|
||||
locationListener
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
// Handle permission issues
|
||||
_locationState.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val locationListener = object : LocationListener {
|
||||
// Methods implementation …
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Extensive Dependencies
|
||||
A large number of dependencies can increase the coupling between the
|
||||
ViewModel and external components, such as repositories, managers, or
|
||||
services. This can reduce the modularity of the code, making it challenging to
|
||||
isolate and reuse the ViewModel in different contexts or parts of the
|
||||
application. Besides that, it can make your codebase less flexible and
|
||||
adaptable to changes. That's because as the ViewModel relies heavily on
|
||||
specific external components, any changes to those components might
|
||||
necessitate modifications to the ViewModel, creating a ripple effect across the
|
||||
codebase.
|
||||
|
||||
Finally, extensive dependencies often involve complex interactions
|
||||
with external services or repositories, making it difficult to create isolated unit
|
||||
tests for the ViewModel. Testing becomes cumbersome and may require
|
||||
extensive setup, leading to slower and less focused unit tests. The complexity
|
||||
of dependencies may also hinder the creation of mock objects for testing. See
|
||||
the example below:
|
||||
|
||||
```kotlin
|
||||
class ExtensiveDependenciesViewModel(
|
||||
private val userRepository: UserRepository,
|
||||
private val taskRepository: TaskRepository,
|
||||
private val analyticsManager: AnalyticsManager,
|
||||
private val networkManager: NetworkManager,
|
||||
private val locationManager: LocationManager,
|
||||
// ... other dependencies ...
|
||||
) : ViewModel() {
|
||||
|
||||
private val _resultState = MutableStateFlow<Result>(Result.Loading)
|
||||
val resultState: StateFlow<Result> get() = _resultState
|
||||
|
||||
init {
|
||||
// Initial loading of data for various features
|
||||
loadData()
|
||||
}
|
||||
|
||||
private fun loadData() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// Fetching user details from a remote server
|
||||
val remoteUserDetails = userRepository.fetchUserDetails()
|
||||
|
||||
// Fetching and processing tasks
|
||||
val remoteTasks = taskRepository.fetchTasks()
|
||||
// Sending analytics events
|
||||
analyticsManager.logEvent("DataLoaded")
|
||||
// Network connectivity check
|
||||
if (networkManager.isNetworkConnected()) {
|
||||
// Additional logic requiring network connectivity
|
||||
// ...
|
||||
}
|
||||
// Location-related operations
|
||||
val currentLocation = locationManager.getCurrentLocation()
|
||||
// Combine results and update state
|
||||
_resultState.value = combineResults(remoteUserDetails, remoteTasks, currentLocation)
|
||||
} catch (e: Exception) {
|
||||
// Handle errors and update UI accordingly
|
||||
_resultState.value = Result.Error(e.message ?: "An error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun combineResults(
|
||||
userDetails: UserDetails,
|
||||
tasks: List<Task>,
|
||||
location: Location?
|
||||
): Result {
|
||||
// Heavy logic for combining user details, tasks, and location
|
||||
// ...
|
||||
return Result.Success(/* Combined result data */)
|
||||
}
|
||||
|
||||
// ... other methods related to extensive dependencies ...
|
||||
companion object {
|
||||
// ... constants or other shared properties ...
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Notice that all the symptoms listed above have one thing in common: they make your
|
||||
ViewModel difficult to test. Understanding that all those symptoms share this common
|
||||
trait—hindering the testability of your ViewModel—can be the first step toward crafting a
|
||||
robust and maintainable architecture. Recognizing these signs of a 'diseased'
|
||||
ViewModel equips you with the insight needed to administer targeted solutions,
|
||||
ensuring a streamlined testing process and enhancing the overall resilience of your
|
||||
application.
|
||||
|
||||
Now, let's explore the remedies that will not only alleviate the identified symptoms but
|
||||
also foster a ViewModel that thrives in the realm of effective testing and code quality.
|
||||
|
||||
## Treating a diseased ViewModel
|
||||
|
||||
Let's consider a hypothetical example of a `BadPracticeViewModel` in Kotlin that
|
||||
incorporates several bad practices, including heavy logic, extensive dependencies,
|
||||
direct Android framework reference and a large scope:
|
||||
|
||||
```kotlin
|
||||
@HiltViewModel
|
||||
class BadPracticeViewModel @Inject constructor(
|
||||
context: Context,
|
||||
private val userRepository: UserRepository,
|
||||
private val taskRepository: TaskRepository,
|
||||
private val analyticsManager: AnalyticsManager,
|
||||
private val networkManager: NetworkManager,
|
||||
// ... other dependencies ...
|
||||
) : ViewModel() {
|
||||
|
||||
private val locationManager: LocationManager by lazy {
|
||||
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
}
|
||||
|
||||
private val _resultState = MutableStateFlow<Result>(Result.Loading)
|
||||
val resultState: StateFlow<Result> get() = _resultState
|
||||
|
||||
init {
|
||||
// Initial loading of data for various features
|
||||
loadData()
|
||||
startLocationUpdates()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun loadData() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// Simulate fetching user details from a remote server
|
||||
val remoteUserDetails = userRepository.fetchUserDetails()
|
||||
// Simulate fetching and processing tasks
|
||||
val remoteTasks = taskRepository.fetchTasks()
|
||||
// Simulate sending analytics events
|
||||
analyticsManager.logEvent("DataLoaded")
|
||||
// Simulate network connectivity check
|
||||
if (networkManager.isNetworkConnected()) {
|
||||
// Additional logic requiring network connectivity
|
||||
// ...
|
||||
}
|
||||
|
||||
// Simulate heavy logic for combining user details, tasks, and location
|
||||
val currentLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
||||
val combinedResult = combineResults(remoteUserDetails, remoteTasks, currentLocation)
|
||||
|
||||
// Update state with the combined result
|
||||
_resultState.value = combinedResult
|
||||
} catch (e: Exception) {
|
||||
// Handle errors and update UI accordingly
|
||||
_resultState.value = Result.Error(e.message ?: "An error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLocationUpdates() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
MIN_TIME_BETWEEN_UPDATES,
|
||||
MIN_DISTANCE_CHANGE_FOR_UPDATES,
|
||||
locationListener,
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
// Handle permission issues
|
||||
_resultState.value = Result.Error("Location permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val locationListener = LocationListener {
|
||||
// Handle location updates
|
||||
}
|
||||
|
||||
private suspend fun combineResults(
|
||||
userDetails: UserDetails,
|
||||
tasks: List<Task>,
|
||||
currentLocation: Location?,
|
||||
): Result {
|
||||
|
||||
// Simulate heavy logic for combining user details and tasks
|
||||
// ...
|
||||
return Result.Success("$userDetails - ${tasks.first()} - $currentLocation")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIN_TIME_BETWEEN_UPDATES: Long = 1000
|
||||
private const val MIN_DISTANCE_CHANGE_FOR_UPDATES: Float = 10f
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
data object Loading : Result()
|
||||
data class Success(val data: String) : Result()
|
||||
data class Error(val message: String) : Result()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As you noticed, we have some bad practices incorporated in the code above.
|
||||
Now, let's discuss why this example incorporates bad practices, why it's advisable to
|
||||
avoid such an approach and refactor it following the best practices:
|
||||
|
||||
|
||||
### 1. Heavy Logic
|
||||
**Issue:** The ViewModel is responsible for fetching data, handling network connectivity
|
||||
checks, obtaining location updates, and combining results.
|
||||
|
||||
**Solution:** Apply the Single Responsibility Principle (SRP) separating each responsibility
|
||||
to a different code unit.
|
||||
|
||||
### 2. Large ViewModel
|
||||
**Issue:** The ViewModel handles multiple features and operations, resulting in a larger
|
||||
class with increased complexity.
|
||||
|
||||
**Solution:** Considering that we already separated concerns effectively, consider now
|
||||
breaking down complex UIs into smaller, reusable components or sub-ViewModels. Use
|
||||
ViewModel composition to combine multiple ViewModels into a single, cohesive UI.
|
||||
Each sub-ViewModel can be responsible for managing a specific part of the UI, such as
|
||||
a list item or a form field.
|
||||
|
||||
### 3. Direct References to Android Framework Components
|
||||
**Issue:** The ViewModel directly references the `LocationManager`, tightly coupling it with
|
||||
Android-specific functionality.
|
||||
|
||||
**Solution:** Consider moving them to separate classes outside of the ViewModel. This
|
||||
could be achieved by using a coordinator or presenter pattern, where the ViewModel
|
||||
delegates Android-specific operations to dedicated classes. Also, dependency injection
|
||||
libraries can help with that.
|
||||
|
||||
### 4. Extensive Dependencies
|
||||
**Issue:** The ViewModel depends on multiple external components, including
|
||||
repositories, managers, and Android framework components.
|
||||
|
||||
**Solution:** Introduce [use cases](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) or
|
||||
[interactors](https://proandroiddev.com/why-you-need-use-cases-interactors-142e8a6fe576) to encapsulate complex business logic and
|
||||
data operations.
|
||||
|
||||
|
||||
In practice, it's crucial to design ViewModels with a clear separation of concerns,
|
||||
minimal dependencies, and a focus on managing UI-related concerns. Adopting clean
|
||||
architecture principles and employing appropriate design patterns can lead to more
|
||||
maintainable, testable, and scalable code.
|
||||
|
||||
After applying some refactorings to the ViewModel above, here's the result:
|
||||
|
||||
|
||||
```kotlin
|
||||
@HiltViewModel
|
||||
class BadPracticeViewModel @Inject constructor(
|
||||
private val locationManager: LocationManager,
|
||||
private val userDetailsAndTasksUseCase: UserDetailsAndTasksUseCase,
|
||||
private val userDetailsAndTasksResultMapper: UserDetailsAndTasksResultMapper,
|
||||
// ... other dependencies ...
|
||||
) : ViewModel() {
|
||||
|
||||
private val _resultState = MutableStateFlow<Result>(Result.Loading)
|
||||
val resultState: StateFlow<Result> get() = _resultState
|
||||
|
||||
init {
|
||||
// Initial loading of data for various features
|
||||
loadData()
|
||||
startLocationUpdates()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun loadData() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val (userDetails, tasks, currentLocation) =
|
||||
userDetailsAndTasksUseCase.fetchUserDetailsAndTasksWithLocation()
|
||||
val combinedResults = userDetailsAndTasksResultMapper.map(userDetails, tasks, currentLocation)
|
||||
// Update state with the combined result
|
||||
_resultState.value = combinedResults
|
||||
} catch (e: Exception) {
|
||||
// Handle errors and update UI accordingly
|
||||
_resultState.value = Result.Error(e.message ?: "An error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLocationUpdates() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
MIN_TIME_BETWEEN_UPDATES,
|
||||
MIN_DISTANCE_CHANGE_FOR_UPDATES,
|
||||
locationListener,
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
// Handle permission issues
|
||||
_resultState.value = Result.Error("Location permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val locationListener = LocationListener {
|
||||
// Handle location updates
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIN_TIME_BETWEEN_UPDATES: Long = 1000
|
||||
private const val MIN_DISTANCE_CHANGE_FOR_UPDATES: Float = 10f
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
data object Loading : Result()
|
||||
data class Success(val data: String) : Result()
|
||||
data class Error(val message: String) : Result()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Diving a little deeper into treatment with States
|
||||
You could notice the usage of UI State data structures in the previous examples, like the
|
||||
`sealed class Result`. All these possible Results are immutable and this state is exposed
|
||||
in one place: `resultState`. This approach is an excellent remedy when treating a
|
||||
diseased ViewModel that's hard to test because we would have a finite number of
|
||||
possible view states to validate and a
|
||||
[Single Source of Truth (SSOT)](https://developer.android.com/topic/architecture#single-source-of-truth).
|
||||
|
||||
This SSOT approach promotes consistency and reliability in data management within
|
||||
the application. It helps in maintaining a clear separation of concerns by centralizing
|
||||
data operations in the ViewModel, which acts as a mediator between the UI and the
|
||||
underlying data sources. This architecture facilitates the implementation of
|
||||
[Unidirectional Data Flow (UDF)](https://developer.android.com/topic/architecture?hl=pt-br#unidirectional-data-flow),
|
||||
where data flows in a single direction—from the ViewModel to the UI components—avoiding circular dependencies and ensuring
|
||||
predictable data flow throughout the application lifecycle.
|
||||
|
||||
|
||||

|
||||
|
||||
In the above diagram we can see clearly how the UDF works with ViewModels:
|
||||
|
||||
1. The ViewModel holds and exposes UI State;
|
||||
2. UI notifies ViewModel of events (button click, for example);
|
||||
3. ViewModel handles the events, updates the state, and is consumed by the UI;
|
||||
4. Repeat the flow.
|
||||
|
||||
Independent of the architecture used: MVVM, MVI, MVP, etc, focus on how to make
|
||||
your UI Data predictable, immutable, and unidirectional. By doing that, your application
|
||||
will be easier to understand, test, and maintain, ultimately leading to better user
|
||||
experiences.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Crafting test-friendly ViewModels is a common challenge faced by developers, but it can
|
||||
be overcome with the right approach. By recognizing the symptoms of a poorly
|
||||
designed ViewModel – such as heavy logic, extensive dependencies, direct references
|
||||
to Android framework components, and overly large scope – we can take steps to
|
||||
address these issues and improve the overall quality and maintainability of our
|
||||
codebase.
|
||||
|
||||
Through this journey, we've explored the core principles of ViewModel design and why
|
||||
adhering to best practices is essential. By applying concepts like the Single
|
||||
Responsibility Principle, separating concerns, Single Source of Truth, Unidirectional
|
||||
Data Flow, and employing clean architecture principles, we can refactor our ViewModels
|
||||
to be more modular, testable, and resilient.
|
||||
|
||||
And finally, be patient. Refactor your code little by little with the help of the existing tests.
|
||||
You don't want a healthy ViewModel with wrong behaviors.
|
||||
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
---
|
||||
title: "How to Access Test Only Files From Unit and Instrumented Test Packages"
|
||||
date: 2022-02-19
|
||||
lastmod: 2024-03-24
|
||||
description: "Learn how to make files available from different test packages in Android development."
|
||||
featured_image: "/img/how-to-access-test-only-files-from-unit-and-instrumented-test-packages/featured_image.webp"
|
||||
draft: false
|
||||
---
|
||||
|
||||

|
||||
|
||||
## TL;DR
|
||||
|
||||
Create a directory called `testCommon` and add the code below to your `build.gradle` file.
|
||||
|
||||
```groovy
|
||||
android {
|
||||
...
|
||||
sourceSets {
|
||||
test { java.srcDirs += src/testCommon }
|
||||
androidTest { java.srcDirs += src/testCommon }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Have you ever been in a situation where you need to access util files from both `test`
|
||||
and `androidTest` packages? If so, this article may be useful for you.
|
||||
|
||||
Let's say that you have a file called `Placeholders.kt` which you use its values
|
||||
as **test doubles** for your unit tests.
|
||||
|
||||
Our test scenario in this case will be the `Success` return of the login method from an
|
||||
use case class called `LoginUseCaseImpl`.
|
||||
|
||||
For example:
|
||||
|
||||
```kotlin
|
||||
// test/Placeholders.kt
|
||||
object Placeholders {
|
||||
const val email = "johndoe@email.com"
|
||||
const val password = "strongpassword123"
|
||||
}
|
||||
|
||||
// test/LoginUseCaseImplTest.kt
|
||||
class LoginUseCaseImplTest {
|
||||
|
||||
private val loginUseCase = LoginUseCaseImpl()
|
||||
|
||||
@Test
|
||||
fun `when call login should return Success`() {
|
||||
// Arrange
|
||||
val email = Placeholders.email
|
||||
val password = Placeholders.password
|
||||
|
||||
// Act
|
||||
val result = loginUseCase.login(email, password)
|
||||
|
||||
// Assert
|
||||
assertEquals(Success, result)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, we need to write **Instrumented Tests** using [Espresso](https://developer.android.com/training/testing/espresso/)
|
||||
to validate the complete login flow. The scenario is: when fill the email and password fields and tap the login button
|
||||
then a success message will display.
|
||||
|
||||
```kotlin
|
||||
// androidTest/LoginScreenInstrumentedTest.kt
|
||||
class LoginScreenInstrumentedTest {
|
||||
@Test
|
||||
fun when_fill_the_email_and_password_fields_and_tap_the_login_button_then_a_success_message_will_display() {
|
||||
// Arrange
|
||||
val email = Placeholders.email
|
||||
val password = Placeholders.password
|
||||
|
||||
// When fill email and password fields
|
||||
onView(withId(R.id.email_field)).perform(ViewActions.typeText(email))
|
||||
onView(withId(R.id.password_field)).perform(ViewActions.typeText(password))
|
||||
|
||||
// And tap the login button
|
||||
onView(withId(R.id.login_button)).perform(ViewActions.click())
|
||||
|
||||
// Then a success message will display.
|
||||
onView(withId(R.id.success_message)).check(matches(isDisplayed()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By default, the espresso test will be inside the `androidTest` package but our `Placeholders.kt`
|
||||
is available only inside the `test` package, which our `LoginUseCaseImplTest` is also located.
|
||||
So, the Instrumented Test above will not find the `Placeholders.kt`.
|
||||
|
||||
To allow both tests access the same file, we need to create a new package inside `src` which we will place the `Placeholders.kt`.
|
||||
In this example, we'll name it as `testCommon`. After that, we need to tell gradle to consider this new package
|
||||
as a `test` and `androidTest` package. We will put the code below in our `build.gradle` file:
|
||||
|
||||
```groovy
|
||||
android {
|
||||
...
|
||||
sourceSets {
|
||||
test { java.srcDirs += src/testCommon }
|
||||
androidTest { java.srcDirs += src/testCommon }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
That's it! Now both test packages will be able to access our `Placeholders.kt` file!
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
---
|
||||
title: "How to Deploy React Applications Using Github Actions + Rsync"
|
||||
date: 2022-10-15T15:30:22-03:00
|
||||
lastmod: 2024-03-24
|
||||
description: "Learn how to easily deploy a react application"
|
||||
featured_image: "/img/how-to-deploy-react-applications-using-github-actions-+-rsync/featured_image.webp"
|
||||
draft: false
|
||||
---
|
||||
|
||||

|
||||
|
||||
Source code and deployed app used for this post:
|
||||
- [rsync-deploy-react-app](https://github.com/leomurca/rsync-deploy-react-app);
|
||||
- [tutorials.leomurca.xyz/rsync-deploy-react-app](https://tutorials.leomurca.xyz/rsync-deploy-react-app/);
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Create a user in your server to deploy your application:
|
||||
```shell
|
||||
$ useradd -s /bin/bash -d /home/tutorials -m tutorials
|
||||
$ su tutorials
|
||||
```
|
||||
|
||||
- Create a folder to copy your production files to:
|
||||
```shell
|
||||
$ mkdir rsync-deploy-react-app
|
||||
```
|
||||
|
||||
- Add your private tutorials's user ssh key to your [Action Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets).
|
||||
|
||||
- Create a new github action to deploy your application and paste the code below:
|
||||
```yml
|
||||
# .github/workflows/deploy.yml
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SSH_KEY: ${{secrets.SSH_KEY}}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'npm'
|
||||
- run: mkdir ~/.ssh
|
||||
- run: 'echo "$SSH_KEY" >> ~/.ssh/id_rsa_tutorials'
|
||||
- run: chmod 400 ~/.ssh/id_rsa_tutorials
|
||||
- run: echo -e "Host tutorials\n\tUser tutorials\n\tHostname 45.76.5.44\n\tIdentityFile ~/.ssh/id_rsa_tutorials\n\tStrictHostKeyChecking No" >> ~/.ssh/config
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: rsync -avz --progress build/ tutorials:/home/tutorials/rsync-deploy-react-app --delete
|
||||
```
|
||||
|
||||
Pay attention to the **Pre-requisites**. That's it! Change some code, push it to the main branch and see the magic happening!
|
||||
|
||||
## Motivation
|
||||
|
||||
I've been working on my bachelor's thesis in information systems, which is a simple React application, and I was struggling to find a **simple, secure and fast** way to deploy it to my own [VPS](https://en.wikipedia.org/wiki/Virtual_private_server). However, most of the content that I had found on the internet involves **fancy and complex** solutions like [docker](https://www.docker.com/) and [Kubernetes](https://kubernetes.io/) or **vendor locked** solutions like [Heroku](https://www.heroku.com/) and [Vercel](https://vercel.com/).
|
||||
|
||||
I recognize these tools have their advantages, but have found that for small to medium sized projects they are more effort than they are worth to maintain. All I need to do is build the code and copy the built files to the server. Then, [rsync](https://en.wikipedia.org/wiki/Rsync) came to my knowledge.
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
- An existing React Application;
|
||||
- An server or a hosting service to deploy your application;
|
||||
- Your own domain registered;
|
||||
- [NGINX](https://www.nginx.com/) installed in your server;
|
||||
- Some knowledge about how to register a domain and create a server on some hosting platform (I'll add articles about that in the future).
|
||||
|
||||
## Demo App to Deploy
|
||||
|
||||
I've created an demo app to deploy it to my server. Its source code is available at [rsync-deploy-react-app](https://github.com/leomurca/rsync-deploy-react-app).
|
||||
|
||||

|
||||
|
||||
## Server Setup
|
||||
|
||||
For this tutorial, I'll use my domain `leomurca.xyz` setting up a sub-domain for it. To be more specific, I'll point `tutorial.leomurca.xyz` to my **VPS's** IP: `45.76.5.44`.
|
||||
|
||||
### SSH to your server
|
||||
|
||||
```shell
|
||||
$ ssh root@45.76.5.44
|
||||
```
|
||||
|
||||
### Create a user to manage your application
|
||||
|
||||
To prevent our pipeline to have root access to your server, I'll create a user to manage deployments called `tutorials`:
|
||||
|
||||
```shell
|
||||
$ useradd -s /bin/bash -d /home/tutorials -m tutorials
|
||||
```
|
||||
|
||||
After that, change the user to it:
|
||||
|
||||
```shell
|
||||
$ su tutorials
|
||||
```
|
||||
|
||||
As I created the user named `tutorials`, this user will host for multiple tutorials, so in order to isolate our application, create a specific folder to house our build files:
|
||||
|
||||
```shell
|
||||
$ mkdir rsync-deploy-react-app
|
||||
```
|
||||
|
||||
## Github Action Setup
|
||||
|
||||
Now let's create the `.github/workflows/deploy.yml` to define the pipeline steps. First, add the label for the workflow and when it should be triggered:
|
||||
|
||||
```yml
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
```
|
||||
|
||||
Above, the workflow will be trigered every time that new code is **pushed** or **merged** to the `main` branch (This happens for Pull Requests merged to the main branch too).
|
||||
|
||||
Then, describe a new job that we will name it as `build-and-deploy` to handle all the steps to build and deploy our app:
|
||||
|
||||
```yml
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SSH_KEY: ${{secrets.SSH_KEY}}
|
||||
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
The `SSH_KEY: ${{secrets.SSH_KEY}}` references [Github Secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) that is allowed to login in our server. It's important to mention that we should add the secret to our repository settings.
|
||||
|
||||
Also, to avoid issues when authenticating to your server using ssh, **use RSA generated keys to authenticate instead of Ed25519 keys**. For more details on that, check this doc on [how to generate a new SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent).
|
||||
|
||||
Afterwards, we'll start define the actual steps to be executed:
|
||||
|
||||
```yml
|
||||
...
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'npm'
|
||||
...
|
||||
```
|
||||
|
||||
These first steps will basically define which [NodeJS](https://nodejs.org/en/) version will be used in our pipeline.
|
||||
|
||||
And now, a very important step is to add the commands that will actually be executed in our workflow, pay attention to them:
|
||||
|
||||
```yml
|
||||
...
|
||||
- run: mkdir ~/.ssh
|
||||
- run: 'echo "$SSH_KEY" >> ~/.ssh/id_rsa_tutorials'
|
||||
- run: chmod 400 ~/.ssh/id_rsa_tutorials
|
||||
- run: echo -e "Host tutorials\n\tUser tutorials\n\tHostname 45.76.5.44\n\tIdentityFile ~/.ssh/id_rsa_tutorials\n\tStrictHostKeyChecking No" >> ~/.ssh/config
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
...
|
||||
```
|
||||
|
||||
The configs above we basically:
|
||||
- Copied the `SSH_KEY` to a file;
|
||||
- Created an ssh config to our server using the key created before;
|
||||
- Downloaded the app dependencies and generate the build files to be copied to our server.
|
||||
|
||||
To make the ssh configs more readable, check the code snippet below:
|
||||
```shell
|
||||
Host tutorials
|
||||
User tutorials
|
||||
Hostname 45.76.5.44
|
||||
IdentityFile ~/.ssh/id_rsa_tutorials
|
||||
StrictHostKeyChecking No
|
||||
```
|
||||
|
||||
### Using rsync to deploy to the server
|
||||
|
||||
And finally, add the rsync command to sync the files from the `build/` folder to our server:
|
||||
|
||||
```yml
|
||||
...
|
||||
- run: rsync -avz --progress build/ tutorials:/home/tutorials/rsync-deploy-react-app --delete
|
||||
...
|
||||
```
|
||||
|
||||
The meaning of each flag used are:
|
||||
- `-a`: It is a quick way of saying you want recursion and want to preserve almost everything (with -H being a notable omission);
|
||||
- `-v` (`-verbose`): This option increases the amount of information the daemon logs during its startup phase.
|
||||
- `-z` (`-compress`): compresses the file data as it is sent to the destination machine, which reduces the amount of data being transmitted -- something that is useful over a slow connection.
|
||||
- `--progress`: This option tells rsync to print information showing the progress of the transfer. This gives a bored user something to watch.
|
||||
- `--delete`: This tells rsync to delete extraneous files from the receiving side (ones that aren't on the sending side), but only for the directories that are being synchronized.
|
||||
|
||||
To have more details on all the options for `rsync`, check its [man page](https://linux.die.net/man/1/rsync).
|
||||
|
||||
### Complete `.github/workflows/deploy.yml`
|
||||
|
||||
```yml
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SSH_KEY: ${{secrets.SSH_KEY}}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'npm'
|
||||
- run: mkdir ~/.ssh
|
||||
- run: 'echo "$SSH_KEY" >> ~/.ssh/id_rsa_tutorials'
|
||||
- run: chmod 400 ~/.ssh/id_rsa_tutorials
|
||||
- run: echo -e "Host tutorials\n\tUser tutorials\n\tHostname 45.76.5.44\n\tIdentityFile ~/.ssh/id_rsa_tutorials\n\tStrictHostKeyChecking No" >> ~/.ssh/config
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: rsync -avz --progress build/ tutorials:/home/tutorials/rsync-deploy-react-app --delete
|
||||
```
|
||||
|
||||
That's it! Change some code, push it to the main branch and see the magic happening!
|
||||
|
||||

|
||||
|
||||
Also, if you want to have more details on the action steps, please check the [actions-executed](https://github.com/leomurca/rsync-deploy-react-app/actions) during this article.
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Simple, fast and secure**, that are the main benefits of using the workflow mentioned in this tutorial. It's really a relief to have these kind of tools in the middle of many bloated solutions.
|
||||
|
||||
If you have any questions or topics to talk about, please [reach me out](/contact)!
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
---
|
||||
title: "How to Push to Multiple Git Remotes With One Command"
|
||||
date: 2022-10-11T16:51:53-03:00
|
||||
lastmod: 2024-03-24
|
||||
description: "Learn how to how to manage multiple remote repositories with git (terminal)."
|
||||
featured_image: "/img/how-to-push-to-multiple-git-remotes-with-one-command/featured_image.webp"
|
||||
draft: false
|
||||
---
|
||||
|
||||

|
||||
Repositories used for the tutorial:
|
||||
|
||||
- [gh-remote](https://github.com/leomurca/gh-remote);
|
||||
- [gl-remote](https://gitlab.com/leomurca/gl-remote.git).
|
||||
|
||||
## TL;DR
|
||||
- Clone the primary repository ([gh-remote](https://github.com/leomurca/gh-remote));
|
||||
|
||||
```shell
|
||||
$ git clone git@github.com:leomurca/gh-remote.git
|
||||
```
|
||||
|
||||
- Add the secondary remote ([gl-remote](https://gitlab.com/leomurca/gl-remote.git)) to the cloned folder;
|
||||
|
||||
```shell
|
||||
$ git remote add gitlab git@gitlab.com:leomurca/gl-remote.git
|
||||
```
|
||||
|
||||
- Add a third remote that will be used to push to all the remotes at the same time. For conveniece, we'll name it as `all`. Also, as the url, we'll use the value `fetch-not-supported`;
|
||||
|
||||
```shell
|
||||
$ git remote add all fetch-not-supported
|
||||
```
|
||||
|
||||
- Add the remotes that you want your code to be pushed when `git push all <BRANCH>` is executed;
|
||||
|
||||
```shell
|
||||
$ git remote set-url --add --push all git@gitlab.com:leomurca/gl-remote.git
|
||||
$ git remote set-url --add --push all git@github.com:leomurca/gh-remote.git
|
||||
```
|
||||
|
||||
- And finally to test if it is working, change some code locally, run the command below and check your both remotes (Be aware of the branch that you are using. In our example, we are using the `main` branch).
|
||||
|
||||
```shell
|
||||
$ git push all main
|
||||
Enumerating objects: 5, done.
|
||||
Counting objects: 100% (5/5), done.
|
||||
Writing objects: 100% (3/3), 275 bytes | 91.00 KiB/s, done.
|
||||
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||
To gitlab.com:leomurca/gl-remote.git
|
||||
584aa2b..1a17aa1 main -> main
|
||||
Enumerating objects: 8, done.
|
||||
Counting objects: 100% (8/8), done.
|
||||
Delta compression using up to 10 threads
|
||||
Compressing objects: 100% (2/2), done.
|
||||
Writing objects: 100% (6/6), 512 bytes | 85.00 KiB/s, done.
|
||||
Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||
To github.com:leomurca/gh-remote.git
|
||||
7e0fb66..1a17aa1 main -> main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
This tutorial is pretty straightforward on how to simplify the synchronization across multiple git remotes, I recommend to save it to your bookmark if you forget any steps.
|
||||
|
||||
## Why to push to multiple git remotes?
|
||||
|
||||
The main use case for pusing to multiple git remotes is to synchronize your changes among all the mirrors used as redundancy for your project. If that is your case (or even if it isn't but you want to learn something new), let's dive in.
|
||||
|
||||
## Adding multiple remotes
|
||||
|
||||
First of all, you need to have the primary git repository cloned to your local machine. You can also start by creating a local repository and then pushing to a remote, but for this tutorial, we'll keep it simple. So, let's clone our primary repository, which in this case, is the [gh-remote](https://github.com/leomurca/gh-remote).
|
||||
|
||||
### Cloning the repo (with the primary remote)
|
||||
|
||||
```shell
|
||||
$ git clone git@github.com:leomurca/gh-remote.git
|
||||
```
|
||||
|
||||
After cloning it, check the default remote assigned to it.
|
||||
|
||||
```shell
|
||||
$ git remote -v
|
||||
origin git@github.com:leomurca/gh-remote.git (fetch)
|
||||
origin git@github.com:leomurca/gh-remote.git (push)
|
||||
```
|
||||
|
||||
### Adding a second remote
|
||||
|
||||
You can notice that the default remote name assigned to the primary repo was `origin`, but you can also change it in the future. Right, now let's just add the second remote ([gl-remote](https://gitlab.com/leomurca/gl-remote.git)) to the cloned folder and then list all the remotes afterwards;
|
||||
|
||||
```shell
|
||||
$ git remote add gitlab git@gitlab.com:leomurca/gl-remote.git
|
||||
$ git remote -v
|
||||
gitlab git@gitlab.com:leomurca/gl-remote.git (fetch)
|
||||
gitlab git@gitlab.com:leomurca/gl-remote.git (push)
|
||||
origin git@github.com:leomurca/gh-remote.git (fetch)
|
||||
origin git@github.com:leomurca/gh-remote.git (push)
|
||||
```
|
||||
|
||||
### Adding a third remote (push only)
|
||||
We'll add a third remote that will be used to push to all the remotes at the same time. For conveniece, we'll name it as `all`. Also, as the url, we'll use the value `fetch-not-supported`. After that, list the remotes.
|
||||
|
||||
```shell
|
||||
$ git remote add all fetch-not-supported
|
||||
$ git remote -v
|
||||
all fetch-not-supported (fetch)
|
||||
all fetch-not-supported (push)
|
||||
gitlab git@gitlab.com:leomurca/gl-remote.git (fetch)
|
||||
gitlab git@gitlab.com:leomurca/gl-remote.git (push)
|
||||
origin git@github.com:leomurca/gh-remote.git (fetch)
|
||||
origin git@github.com:leomurca/gh-remote.git (push)
|
||||
```
|
||||
|
||||
And finally, we'll add the remotes that we want our code to be pushed when `git push all <BRANCH>` is executed;
|
||||
|
||||
```shell
|
||||
$ git remote set-url --add --push all git@gitlab.com:leomurca/gl-remote.git
|
||||
$ git remote set-url --add --push all git@github.com:leomurca/gh-remote.git
|
||||
```
|
||||
|
||||
## List all remotes
|
||||
|
||||
Let's see the final result listing all the remotes added before.
|
||||
|
||||
```shell
|
||||
$ git remote -v
|
||||
all fetch-not-supported (fetch)
|
||||
all git@gitlab.com:leomurca/gl-remote.git (push)
|
||||
all git@github.com:leomurca/gh-remote.git (push)
|
||||
gitlab git@gitlab.com:leomurca/gl-remote.git (fetch)
|
||||
gitlab git@gitlab.com:leomurca/gl-remote.git (push)
|
||||
origin git@github.com:leomurca/gh-remote.git (fetch)
|
||||
origin git@github.com:leomurca/gh-remote.git (push)
|
||||
```
|
||||
|
||||
## Pushing to multiple remotes
|
||||
|
||||
And now that everything is set up, let's test if it is working: change some code locally, run the command below and check your both remotes (Be aware of the branch that you are using. In our example, we are using the `main` branch).
|
||||
|
||||
```shell
|
||||
$ git push all main
|
||||
Enumerating objects: 5, done.
|
||||
Counting objects: 100% (5/5), done.
|
||||
Writing objects: 100% (3/3), 275 bytes | 91.00 KiB/s, done.
|
||||
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||
To gitlab.com:leomurca/gl-remote.git
|
||||
584aa2b..1a17aa1 main -> main
|
||||
Enumerating objects: 8, done.
|
||||
Counting objects: 100% (8/8), done.
|
||||
Delta compression using up to 10 threads
|
||||
Compressing objects: 100% (2/2), done.
|
||||
Writing objects: 100% (6/6), 512 bytes | 85.00 KiB/s, done.
|
||||
Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||
To github.com:leomurca/gh-remote.git
|
||||
7e0fb66..1a17aa1 main -> main
|
||||
```
|
||||
|
||||
That's all! Enjoy your new git configuration.
|
||||
|
||||
## Extra
|
||||
|
||||
Do want to fetch from multiple remotes? Just execute `git fetch --all`.
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
---
|
||||
title: "Embroidery Viewer"
|
||||
description: "A free online tool to view embroidery files built with Svelte."
|
||||
lead_video: "/img/embroidery-viewer/lead-video.mp4"
|
||||
featured_image: "/img/embroidery-viewer/card-screenshot.png"
|
||||
status: "Live"
|
||||
draft: false
|
||||
---
|
||||
|
||||
## About the Project
|
||||
|
||||
**Embroidery Viewer** is a lightweight, privacy-focused, and open-source web application that enables users to preview embroidery files instantly in their browser. It supports a wide range of embroidery file formats, including `.PES`, `.DST`, `.EXP`, `.JEF`, and others.
|
||||
|
||||
The tool runs entirely client-side, meaning files never leave the user’s device, ensuring complete privacy. With a clean and intuitive interface, it allows users to visualize multiple embroidery files at the same time, making it easy to compare and review designs.
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- 📂 Supports Multiple Formats: `.DST`, `.PES`, `.JEF`, `.EXP`, `.VP3`, and more
|
||||
- ⚡ Quick Previews: See your embroidery files rendered as images
|
||||
- 🧷 Multiple Files at Once: Upload several designs and view them side-by-side
|
||||
- 🔒 No Upload to Server: Your files stay private – all processing happens locally
|
||||
- ⬇️ Download as Image: Save each embroidery design preview as a PNG
|
||||
- 💸 Fast & Free: No installations, no sign-ups – just open and use
|
||||
|
||||
## 🔧 Tech Stack
|
||||
|
||||
- **Frontend:** Svelte, HTML5, CSS3
|
||||
- **Rendering:** HTML Canvas and SVG
|
||||
- **Hosting:** Self-hosted on a VPS with Nginx
|
||||
- **Other:** Git for version control, lightweight infrastructure
|
||||
|
||||
## 🤝 Open Source & Contributions
|
||||
|
||||
Embroidery Viewer is an **open-source project**. Whether you are a developer, designer, or someone passionate about embroidery, [you are welcome to contribute](https://git.leomurca.xyz/leomurca/embroidery-viewer).
|
||||
|
||||
## 🚀 Challenges & Solutions
|
||||
|
||||
- **Binary Parsing in Browser:** Implemented parsing of complex binary embroidery files client-side.
|
||||
- **Rendering Stitches:** Converted stitch data into SVG or Canvas paths for accurate visualization.
|
||||
- **Performance:** Optimized to handle multiple files with minimal memory footprint and fast parsing.
|
||||
|
||||
## 📈 Impact
|
||||
|
||||
- Helping hobbyists and embroidery professionals preview files without expensive software.
|
||||
- Gained organic traffic from embroidery communities.
|
||||
- Featured in several hobbyist forums and communities.
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- 🌐 [Visit Website](https://embroideryviewer.xyz)
|
||||
- 👨💻 [Source code](https://git.leomurca.xyz/leomurca/embroidery-viewer)
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
---
|
||||
title: "Entrepreneurship Alagoas"
|
||||
description: "Landing page showcasing Alagoas startup programs with funding and mentorship."
|
||||
lead_video: "/img/entrepreneurship-alagoas/lead-video.mp4"
|
||||
featured_image: "/img/entrepreneurship-alagoas/screenshot.png"
|
||||
status: "Live"
|
||||
draft: false
|
||||
---
|
||||
|
||||
## About the Project
|
||||
|
||||
**Empreendedorismo SECTI Alagoas** is a custom-built landing page created as a freelance project to promote **Lagoon Startup** and **VAI Startup** — two state-backed entrepreneurship programs in Alagoas. These initiatives support local startups and entrepreneurs with funding, mentorship, and structured methodologies to advance their ventures 🚀.
|
||||
|
||||
The site was built using **SvelteKit**, with a focus on a clean, responsive design and optimized performance, presenting program details clearly and effectively to the target audience.
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- 🌱 **Program Promotion**: Detailed presentation of **Lagoon Startup** and **VAI Startup**, including funding, timelines, and eligibility
|
||||
- ✅ **Responsive Layout**: Ensures accessibility and readability on mobile, tablet, and desktop
|
||||
- 🌐 **SEO & Performance Optimization**: Uses semantic HTML and fast loading behavior
|
||||
- 🎯 **Clear Call-to-Action**: Highlights registration deadlines, benefits, and next steps
|
||||
|
||||
### 🔧 Tech Stack
|
||||
|
||||
- **Frontend:** SvelteKit, HTML5, CSS3
|
||||
- **Hosting:** Deployed on a VPS
|
||||
- **Dev Tools:** Git for version control; CI/CD pipeline for streamlined deployment
|
||||
|
||||
### 🤝 My Role
|
||||
|
||||
As a freelance full-stack developer, I was responsible for:
|
||||
|
||||
1. Gathering requirements and aligning design with SECTI’s branding
|
||||
2. Designing a responsive and intuitive interface
|
||||
3. Integrating program content (funding details, schedules, instructions)
|
||||
4. Optimizing the landing page for speed, SEO, and accessibility
|
||||
5. Setting up deployment using CI/CD on a VPS
|
||||
|
||||
### 📈 Impact
|
||||
|
||||
- Engaged local entrepreneurs with clear, organized presentation of startup opportunities
|
||||
- Provided a professional digital presence for SECTI’s programs
|
||||
- Contributed to the visibility and credibility of public support for entrepreneurship in Alagoas
|
||||
|
||||
### 🔗 Links
|
||||
|
||||
- 🌐 [Visit website](https://empreendedorismo.secti.al.gov.br/)
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
---
|
||||
title: "Innovative Technologies for Repair (ITR)"
|
||||
description: "Landing page built with SvelteKit to promote a public call for tech development projects in Brazil, offering training and funding support."
|
||||
lead_video: "/img/itr/lead-video.mp4"
|
||||
featured_image: "/img/itr/screenshot.png"
|
||||
status: "Archived"
|
||||
draft: false
|
||||
---
|
||||
|
||||
## About the Project
|
||||
|
||||
**Innovative Technologies for Repair (ITR)** is a landing page built with **SvelteKit** to promote a public call for technological development projects in Brazil. The site presents key information about the program, including eligibility, benefits, and application details, with a clean design and strong emphasis on clarity and performance.
|
||||
|
||||
The landing page leverages **server-side rendering (SSR)** to improve SEO and loading speed, making it accessible even on slower connections and optimized for discoverability.
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- 📄 **Program Overview**: Details about the selection process, training modules, timeline, and benefits
|
||||
- 🎯 **Conversion-Oriented Design**: Clear structure with prominent calls-to-action and application buttons
|
||||
- ⚡ **SSR with SvelteKit**: Server-side rendering for enhanced performance and SEO
|
||||
- ✅ **Responsive Layout**: Clean, adaptive design for all screen sizes
|
||||
- 🌐 **SEO Optimization**: Semantic markup and fast page load
|
||||
|
||||
### 🔧 Tech Stack
|
||||
|
||||
- **Frontend:** SvelteKit (with SSR), HTML5, CSS3
|
||||
- **Hosting:** Self-hosted on a VPS
|
||||
- **Tooling:** Git for version control, CI/CD for automated deployment
|
||||
|
||||
### 🤝 My Role
|
||||
|
||||
As a freelance full-stack developer, I was responsible for:
|
||||
|
||||
1. Designing the responsive layout aligned with the program's identity
|
||||
2. Structuring and developing the site with performance and clarity in mind
|
||||
3. Configuring server-side rendering with SvelteKit
|
||||
4. Optimizing SEO and accessibility
|
||||
5. Automating deployment using CI/CD on a private server
|
||||
|
||||
### 📈 Impact
|
||||
|
||||
- Helped promote the TIR call for proposals to a broader audience
|
||||
- Provided applicants with clear, accessible, and well-organized information
|
||||
- Supported public visibility for funding opportunities and innovation in Brazil
|
||||
|
||||
### 🔗 Links
|
||||
|
||||
- 🌐 [Live demo](https://projects.leomurca.xyz/tir/)
|
||||
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
---
|
||||
title: "Lacine"
|
||||
description: "Landing page built with SvelteKit to promote Lacine, a creative lab for filmmakers."
|
||||
lead_video: "/img/lacine/lead-video.mp4"
|
||||
featured_image: "/img/lacine/screenshot.png"
|
||||
status: "Archived"
|
||||
draft: false
|
||||
---
|
||||
|
||||
## About the Project
|
||||
|
||||
**Lacine** is a custom website developed as a freelance project for a client in the independent cinema space. The site was designed to showcase film programming and cultural events with a clean aesthetic and user-focused experience.
|
||||
|
||||
Built from the ground up with responsiveness and performance in mind, it delivers fast-loading, accessible content that works seamlessly across devices.
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- 🎬 **Event Listing**: Displays a organic layout with dates and times of each event
|
||||
- ✅ **Responsive Design**: Optimized for mobile, tablet, and desktop
|
||||
- 🌐 **SEO & Performance Optimized**: Semantic structure and fast load times
|
||||
- 💼 **Custom Branding**: Layout tailored to the client’s visual identity
|
||||
- 🎨 **Visual Storytelling**: A cinematic presentation that highlights mood, schedule, and call to action
|
||||
|
||||
## 🔧 Tech Stack
|
||||
|
||||
- **Frontend:** SvelteKit, HTML5, CSS3
|
||||
- **Hosting:** Self-hosted on VPS
|
||||
- **Tooling:** Git for version control, CI/CD for deployment
|
||||
|
||||
## 🤝 My Role
|
||||
|
||||
As a freelance full-stack developer, I was responsible for:
|
||||
|
||||
1. Gathering and refining project requirements with the client
|
||||
2. Designing a responsive and elegant user interface
|
||||
3. Implementing key features (scheduling and event details)
|
||||
4. Optimizing for performance and SEO
|
||||
5. Handling deployment and infrastructure setup
|
||||
6. With the tight schedule, I had to develop everything fast
|
||||
|
||||
## 📈 Impact
|
||||
|
||||
- Helped the client establish a professional online presence with full control over content
|
||||
- The final product stands out for its clarity, aesthetics, and usability in a niche market
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- 🌐 [Live demo](https://projects.leomurca.xyz/lacine/)
|
||||
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
title: "Contato"
|
||||
date: 2022-10-15
|
||||
description: "Caso tenha alguma dúvida, tópicos para falar ou trabalho para oferecer, sinta-se à vontade para entrar em contato comigo."
|
||||
featured_image: "/img/avatar.webp"
|
||||
draft: false
|
||||
---
|
||||
|
||||
Caso tenha alguma dúvida, tópicos para falar ou trabalho para oferecer, sinta-se à vontade para entrar em contato comigo.
|
||||
|
||||
- E-mail: leo@leomurca.xyz
|
||||
- LinkedIn: https://linkedin.com/in/leonardoamurca
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
---
|
||||
title: "Doe"
|
||||
date: 2022-11-16T19:08:42-03:00
|
||||
description: "Se meu trabalho ou conhecimento o ajudou de alguma forma, considere me apoiar financeiramente."
|
||||
featured_image: "/img/avatar.webp"
|
||||
draft: false
|
||||
---
|
||||
|
||||
Se meu trabalho ou conhecimento te ajudou de alguma forma, considere me apoiar financeiramente.
|
||||
|
||||
- {{< icon src="bat.svg" alt="Ícone do Brave Attention Token" >}} **BAT (Brave Attention Token)**: Me mande uma contribuição através do [Brave Rewards](https://support.brave.com/hc/en-us/articles/360021123971-How-do-I-tip-websites-and-Content-Creators-in-Brave-Rewards-#:~:text=In%20the%20tipping%20banner%20%2C%20the,tip%20to%20complete%20the%20transaction.).
|
||||
|
||||
- {{< icon src="bitcoin.svg" alt="Ícone do Bitcon" >}} **Bitcoin**: `bc1qpc4lpyr6stxrrg3u0k4clp4crlt6z4j6q845rq`.
|
||||
- {{< icon src="monero.svg" alt="Ícone da Monero" >}} **Monero**: `8A9iyTskiBh6f6GDUwnUJaYhAW13gNjDYaZYJBftX434D3XLrcGBko4a8kC4pLSfiuJAoSJ7e8rwP8W4StsVypftCp6FGwm`.
|
||||
|
||||
Sinta-se à vontade para também me enviar uma mensagem para **leo@leomurca.xyz** após a doação.
|
||||
|
|
@ -1,536 +0,0 @@
|
|||
---
|
||||
title: "Desmistificando os testes de ViewModel: estratégias para criar ViewModels fáceis de testar"
|
||||
date: 2024-03-25T10:54:21-03:00
|
||||
description: "Aprenda as melhores práticas para escrever testes para seu ViewModel."
|
||||
featured_image: "/img/demystifying-viewmodel-testing/featured_image.webp"
|
||||
draft: false
|
||||
---
|
||||
|
||||

|
||||
|
||||
## Introdução
|
||||
|
||||
Você já teve dificuldades ao escrever testes unitários para seu ViewModel? Dificuldades ao
|
||||
escrever testes são um GRANDE sintoma de que seu ViewModel está mal escrito. Se o simples
|
||||
pensamento de testar seu ViewModel te causa arrepios ou se você se vê lutando com setups
|
||||
complicados apenas para verificar um comportamento simples, não tema – você não está sozinho.
|
||||
Escrever ViewModels amigáveis aos testes é um desafio comum enfrentado por muitos desenvolvedores,
|
||||
mas a boa notícia é que é um desafio que pode ser superado.
|
||||
|
||||
Neste post, exploraremos as razões por trás da luta nos testes, desvendando as complexidades
|
||||
do design de ViewModel que levam a dores de cabeça nos testes. Mais importante ainda, iremos equipá-lo
|
||||
com insights e técnicas práticas para transformar seus ViewModels em unidades amigáveis aos testes, tornando
|
||||
o processo de teste unitário uma experiência fluida e eficiente. Vamos embarcar em uma jornada para banir as
|
||||
preocupações com testes e elevar o seu jogo de ViewModel!
|
||||
|
||||
## O que é um ViewModel?
|
||||
|
||||
|
||||
Primeiramente, precisamos definir o que é um ViewModel e qual é o seu propósito de existência. De acordo com a
|
||||
[Visão geral do ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel): *"[…] a classe ViewModel
|
||||
é um detentor de estado da tela ou da lógica de negócios"*. Em outras palavras, ele encapsula a lógica de negócios
|
||||
relacionada e expõe o Estado da Interface do Usuário da Tela.
|
||||
|
||||
No entanto, qualquer classe Kotlin simples pode ser usada como uma detentora de estado (StateHolder) para encapsular a lógica
|
||||
de negócios e expor algum Estado da Interface do Usuário da Tela. Então, por que precisamos de ViewModels?
|
||||
|
||||
A principal razão pela qual usamos um ViewModel em vez de uma classe Kotlin simples é que os ViewModels:
|
||||
|
||||
- Sobrevivem a mudanças de configuração (conscientes do ciclo de vida). Essas mudanças de configuração estão relacionadas
|
||||
a um benefício de persistência de dados ao usar ViewModels.
|
||||
- Possuem ótima integração com o Jetpack e outras bibliotecas;
|
||||
- Fazem cache de estados.
|
||||
|
||||
Conhecendo a definição de um ViewModel e os motivos pelos quais devemos usá-lo, devemos implementá-lo e mantê-lo com muito
|
||||
cuidado. E caso seu ViewModel já exista, preste atenção nos sintomas que podem indicar que ele precisa de alguns cuidados.
|
||||
|
||||
## Sintomas que indicam que seu ViewModel precisa de alguns cuidados
|
||||
|
||||
### 1. Lógica Pesada
|
||||
Ter lógica de negócios complexa ou manipulação extensa de dados diretamente no ViewModel pode ser um ótimo indicador de que
|
||||
seu ViewModel precisa de atenção. Este sintoma causará muitas dores de cabeça ao testá-lo e mantê-lo. Observe o
|
||||
`UserProfileViewModel` abaixo, por exemplo:
|
||||
|
||||
```kotlin
|
||||
class UserProfileViewModel(
|
||||
private val userRepository: UserRepository,
|
||||
private val userLocalDataSource: UserLocalDataSource
|
||||
) : ViewModel() {
|
||||
|
||||
private val _userProfileState = MutableStateFlow<UserProfile?>(null)
|
||||
val userProfileState: StateFlow<UserProfile?> get() = _userProfileState
|
||||
|
||||
private val _loadingState = MutableStateFlow<Boolean>(false)
|
||||
val loadingState: StateFlow<Boolean> get() = _loadingState
|
||||
|
||||
init {
|
||||
// Initial loading of user profile
|
||||
loadUserProfile()
|
||||
}
|
||||
|
||||
private fun loadUserProfile() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
_loadingState.emit(true)
|
||||
// Fetching user details from a remote server
|
||||
val remoteUserDetails = userRepository.fetchUserDetails()
|
||||
// Processing and transforming user details
|
||||
val processedUserProfile = processUserProfile(remoteUserDetails)
|
||||
// Updating the local database with the processed data
|
||||
userLocalDataSource.updateUserProfile(processedUserProfile)
|
||||
_userProfileState.emit(processedUserProfile)
|
||||
} catch (e: Exception) {
|
||||
// Handle errors and update UI accordingly
|
||||
} finally {
|
||||
_loadingState.emit(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processUserProfile(userDetails: UserDetails): UserProfile {
|
||||
// Heavy processing and transformation of user details
|
||||
// ...
|
||||
return UserProfile(/* Processed user profile data */)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. ViewModels grandes demais
|
||||
Uma classe grande é um [code smell](https://martinfowler.com/bliki/CodeSmell.html) bem conhecido para classes que têm muitas responsabilidades.
|
||||
Isso não é diferente para ViewModels. A única responsabilidade do ViewModel é gerenciar os dados da UI. Ter ViewModels excessivamente grandes
|
||||
com muitas responsabilidades pode dificultar a compreensão, o teste e a manutenção do código. Esteja ciente de que seguir o
|
||||
[SRP](https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html) também é fundamental para ViewModels.
|
||||
Veja o exemplo abaixo com muitas responsabilidades para um único ViewModel:
|
||||
|
||||
```kotlin
|
||||
class LargeViewModel(
|
||||
private val userRepository: UserRepository,
|
||||
private val taskRepository: TaskRepository,
|
||||
private val analyticsManager: AnalyticsManager,
|
||||
// ... other dependencies ...
|
||||
) : ViewModel() {
|
||||
// Properties for various data streams
|
||||
private val _userProfileState = MutableStateFlow<UserProfile?>(null)
|
||||
|
||||
val userProfileState: StateFlow<UserProfile?> get() = _userProfileState
|
||||
private val _tasksState = MutableStateFlow<List<Task>>(emptyList())
|
||||
|
||||
val tasksState: StateFlow<List<Task>> get() = _tasksState
|
||||
|
||||
// ... Other properties for different features ...
|
||||
|
||||
private val _loadingState = MutableStateFlow<Boolean>(false)
|
||||
val loadingState: StateFlow<Boolean> get() = _loadingState
|
||||
|
||||
init {
|
||||
// Initial loading of data for various features
|
||||
loadData()
|
||||
}
|
||||
|
||||
private fun loadData() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
_loadingState.emit(true)
|
||||
// Fetching user details from a remote server
|
||||
val remoteUserDetails = userRepository.fetchUserDetails()
|
||||
_userProfileState.emit(processUserProfile(remoteUserDetails))
|
||||
// Fetching and processing tasks
|
||||
val remoteTasks = taskRepository.fetchTasks()
|
||||
_tasksState.emit(processTasks(remoteTasks))
|
||||
// ... Load data for other features ...
|
||||
// Sending analytics events
|
||||
analyticsManager.logEvent("DataLoaded")
|
||||
} catch (e: Exception) {
|
||||
// Handle errors and update UI accordingly
|
||||
} finally {
|
||||
_loadingState.emit(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ... Other methods for processing data, handling user interactions, etc. ...
|
||||
private suspend fun processUserProfile(userDetails: UserDetails): UserProfile {
|
||||
// Processing user details
|
||||
// ...
|
||||
return UserProfile(/* Processed user profile data */)
|
||||
}
|
||||
|
||||
private suspend fun processTasks(tasks: List<Task>): List<Task> {
|
||||
// Processing tasks
|
||||
// ...
|
||||
return tasks
|
||||
}
|
||||
// ... Other methods for different features ...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Referências diretas do Framework Android
|
||||
Evite referências diretas a componentes da estrutura Android, como Context ou View, no ViewModel. Isso torna o ViewModel
|
||||
menos testável e pode levar a memory leaks. Se algo precisa de um `context` no ViewModel, você deve avaliar fortemente
|
||||
se ele está na camada correta. ViewModels devem ser projetados para serem testáveis isoladamente da estrutura Android.
|
||||
Não deixe seu ViewModel muito preso a uma estrutura, torne-o o mais agnóstico possível. Um exemplo comum é quando
|
||||
precisamos acessar a localização do dispositivo. Veja `LocationViewModel` abaixo:
|
||||
|
||||
```kotlin
|
||||
class LocationViewModel(private val context: Context) : ViewModel() {
|
||||
|
||||
private val _locationState = MutableStateFlow<Location?>(null)
|
||||
val locationState: StateFlow<Location?> get() = _locationState
|
||||
|
||||
private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
|
||||
init {
|
||||
// Start listening for location updates
|
||||
startLocationUpdates()
|
||||
}
|
||||
|
||||
private fun startLocationUpdates() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
1000,
|
||||
10,
|
||||
locationListener
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
// Handle permission issues
|
||||
_locationState.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val locationListener = object : LocationListener {
|
||||
// Methods implementation …
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Dependências Extensas
|
||||
Um grande número de dependências pode aumentar o acoplamento entre o ViewModel e componentes externos, como repositórios,
|
||||
gerenciadores ou serviços. Isso pode reduzir a modularidade do código, tornando difícil isolar e reutilizar o ViewModel
|
||||
em diferentes contextos ou partes do aplicativo. Além disso, pode tornar sua base de código menos flexível e adaptável
|
||||
a mudanças. Isso ocorre porque, como o ViewModel depende muito de componentes externos específicos, qualquer alteração
|
||||
nesses componentes pode exigir modificações no ViewModel, criando um efeito cascata em toda a base de código.
|
||||
|
||||
Por fim, dependências extensas geralmente envolvem interações complexas com serviços ou repositórios externos, dificultando
|
||||
a criação de testes unitários isolados para o ViewModel. Os testes tornam-se complicados e podem exigir configurações extensas,
|
||||
resultando em testes unitários mais lentos e menos focados. A complexidade das dependências também pode dificultar a
|
||||
criação de objetos simulados para teste. Veja o exemplo abaixo:
|
||||
|
||||
```kotlin
|
||||
class ExtensiveDependenciesViewModel(
|
||||
private val userRepository: UserRepository,
|
||||
private val taskRepository: TaskRepository,
|
||||
private val analyticsManager: AnalyticsManager,
|
||||
private val networkManager: NetworkManager,
|
||||
private val locationManager: LocationManager,
|
||||
// ... other dependencies ...
|
||||
) : ViewModel() {
|
||||
|
||||
private val _resultState = MutableStateFlow<Result>(Result.Loading)
|
||||
val resultState: StateFlow<Result> get() = _resultState
|
||||
|
||||
init {
|
||||
// Initial loading of data for various features
|
||||
loadData()
|
||||
}
|
||||
|
||||
private fun loadData() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// Fetching user details from a remote server
|
||||
val remoteUserDetails = userRepository.fetchUserDetails()
|
||||
|
||||
// Fetching and processing tasks
|
||||
val remoteTasks = taskRepository.fetchTasks()
|
||||
// Sending analytics events
|
||||
analyticsManager.logEvent("DataLoaded")
|
||||
// Network connectivity check
|
||||
if (networkManager.isNetworkConnected()) {
|
||||
// Additional logic requiring network connectivity
|
||||
// ...
|
||||
}
|
||||
// Location-related operations
|
||||
val currentLocation = locationManager.getCurrentLocation()
|
||||
// Combine results and update state
|
||||
_resultState.value = combineResults(remoteUserDetails, remoteTasks, currentLocation)
|
||||
} catch (e: Exception) {
|
||||
// Handle errors and update UI accordingly
|
||||
_resultState.value = Result.Error(e.message ?: "An error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun combineResults(
|
||||
userDetails: UserDetails,
|
||||
tasks: List<Task>,
|
||||
location: Location?
|
||||
): Result {
|
||||
// Heavy logic for combining user details, tasks, and location
|
||||
// ...
|
||||
return Result.Success(/* Combined result data */)
|
||||
}
|
||||
|
||||
// ... other methods related to extensive dependencies ...
|
||||
companion object {
|
||||
// ... constants or other shared properties ...
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
Observe que todos os sintomas listados acima têm uma coisa em comum: eles dificultam o teste do seu ViewModel. Compreender
|
||||
que todos esses sintomas compartilham essa característica comum – dificultando a testabilidade do seu ViewModel – pode ser
|
||||
o primeiro passo para a criação de uma arquitetura robusta e de fácil manutenção. Reconhecer esses sinais de um ViewModel
|
||||
“doente” fornece a você o conhecimento necessário para administrar soluções direcionadas, garantindo um processo de teste
|
||||
simplificado e melhorando a resiliência geral do seu aplicativo.
|
||||
|
||||
Agora, vamos explorar as soluções que não apenas aliviarão os sintomas identificados, mas também promoverão um ViewModel
|
||||
que prospere no domínio dos testes eficazes e da qualidade do código.
|
||||
|
||||
### Tratando um ViewModel doente
|
||||
Vamos considerar um exemplo hipotético: `BadPracticeViewModel` em Kotlin que incorpora várias práticas ruins, incluindo
|
||||
lógica pesada, dependências extensas, referência direta à estrutura Android e um escopo grande:
|
||||
|
||||
```kotlin
|
||||
@HiltViewModel
|
||||
class BadPracticeViewModel @Inject constructor(
|
||||
context: Context,
|
||||
private val userRepository: UserRepository,
|
||||
private val taskRepository: TaskRepository,
|
||||
private val analyticsManager: AnalyticsManager,
|
||||
private val networkManager: NetworkManager,
|
||||
// ... other dependencies ...
|
||||
) : ViewModel() {
|
||||
|
||||
private val locationManager: LocationManager by lazy {
|
||||
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
}
|
||||
|
||||
private val _resultState = MutableStateFlow<Result>(Result.Loading)
|
||||
val resultState: StateFlow<Result> get() = _resultState
|
||||
|
||||
init {
|
||||
// Initial loading of data for various features
|
||||
loadData()
|
||||
startLocationUpdates()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun loadData() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// Simulate fetching user details from a remote server
|
||||
val remoteUserDetails = userRepository.fetchUserDetails()
|
||||
// Simulate fetching and processing tasks
|
||||
val remoteTasks = taskRepository.fetchTasks()
|
||||
// Simulate sending analytics events
|
||||
analyticsManager.logEvent("DataLoaded")
|
||||
// Simulate network connectivity check
|
||||
if (networkManager.isNetworkConnected()) {
|
||||
// Additional logic requiring network connectivity
|
||||
// ...
|
||||
}
|
||||
|
||||
// Simulate heavy logic for combining user details, tasks, and location
|
||||
val currentLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
||||
val combinedResult = combineResults(remoteUserDetails, remoteTasks, currentLocation)
|
||||
|
||||
// Update state with the combined result
|
||||
_resultState.value = combinedResult
|
||||
} catch (e: Exception) {
|
||||
// Handle errors and update UI accordingly
|
||||
_resultState.value = Result.Error(e.message ?: "An error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLocationUpdates() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
MIN_TIME_BETWEEN_UPDATES,
|
||||
MIN_DISTANCE_CHANGE_FOR_UPDATES,
|
||||
locationListener,
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
// Handle permission issues
|
||||
_resultState.value = Result.Error("Location permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val locationListener = LocationListener {
|
||||
// Handle location updates
|
||||
}
|
||||
|
||||
private suspend fun combineResults(
|
||||
userDetails: UserDetails,
|
||||
tasks: List<Task>,
|
||||
currentLocation: Location?,
|
||||
): Result {
|
||||
|
||||
// Simulate heavy logic for combining user details and tasks
|
||||
// ...
|
||||
return Result.Success("$userDetails - ${tasks.first()} - $currentLocation")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIN_TIME_BETWEEN_UPDATES: Long = 1000
|
||||
private const val MIN_DISTANCE_CHANGE_FOR_UPDATES: Float = 10f
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
data object Loading : Result()
|
||||
data class Success(val data: String) : Result()
|
||||
data class Error(val message: String) : Result()
|
||||
}
|
||||
}
|
||||
```
|
||||
Como você percebeu, temos algumas práticas inadequadas incorporadas no código acima. Agora, vamos discutir
|
||||
por que este exemplo incorpora más práticas, por que é aconselhável evitar tal abordagem e refatorá-la seguindo
|
||||
as melhores práticas:
|
||||
|
||||
### 1. Lógica pesada
|
||||
**Problema:** O ViewModel é responsável por buscar dados, lidar com a conectividade de rede
|
||||
verificações, obtenção de atualizações de localização e combinação de resultados.
|
||||
|
||||
**Solução:** Aplique o Princípio da Responsabilidade Única (SRP), separando cada
|
||||
responsabilidade em uma unidade de código diferente.
|
||||
|
||||
|
||||
### 2. ViewModel grande demais
|
||||
**Problema:** o ViewModel lida com vários recursos e operações, resultando em uma classe maior com maior complexidade.
|
||||
|
||||
**Solução:** Considerando que já separamos as preocupações de forma eficaz, considere agora dividir UIs complexas
|
||||
em componentes menores e reutilizáveis ou subViewModels. Use a composição do ViewModel para combinar vários
|
||||
ViewModels em uma UI única e coesa. Cada sub-ViewModel pode ser responsável por gerenciar uma parte específica da
|
||||
UI, como um item de lista ou um campo de formulário.
|
||||
|
||||
### 3. Referências diretas do Framework Android
|
||||
**Problema:** o ViewModel faz referência direta ao `LocationManager`, acoplando-o fortemente à funcionalidade específica do Android.
|
||||
|
||||
**Solução:** considere movê-los para classes separadas fora do ViewModel. Isso poderia ser conseguido usando um padrão presenter, onde
|
||||
o ViewModel delega operações específicas do Android para classes dedicadas. Além disso, bibliotecas de injeção de dependência podem ajudar nisso.
|
||||
|
||||
### 4. Dependências Extensas
|
||||
**Problema:** o ViewModel depende de vários componentes externos, incluindo repositórios, gerenciadores e componentes do Framework Android.
|
||||
|
||||
**Solução:** introduza [use cases](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
ou [interactors](https://proandroiddev.com/why-you-need-use-cases-interactors-142e8a6fe576) para encapsular lógica de negócios complexa
|
||||
e operações de dados.
|
||||
|
||||
Na prática, é crucial projetar ViewModels com uma separação clara de preocupações, dependências mínimas e foco no gerenciamento de preocupações
|
||||
relacionadas à UI. Adotar princípios de arquitetura limpa e empregar padrões de design apropriados pode levar a um código mais sustentável,
|
||||
testável e escalonável.
|
||||
|
||||
Depois de aplicar algumas refatorações ao ViewModel acima, aqui está o resultado:
|
||||
|
||||
```kotlin
|
||||
@HiltViewModel
|
||||
class BadPracticeViewModel @Inject constructor(
|
||||
private val locationManager: LocationManager,
|
||||
private val userDetailsAndTasksUseCase: UserDetailsAndTasksUseCase,
|
||||
private val userDetailsAndTasksResultMapper: UserDetailsAndTasksResultMapper,
|
||||
// ... other dependencies ...
|
||||
) : ViewModel() {
|
||||
|
||||
private val _resultState = MutableStateFlow<Result>(Result.Loading)
|
||||
val resultState: StateFlow<Result> get() = _resultState
|
||||
|
||||
init {
|
||||
// Initial loading of data for various features
|
||||
loadData()
|
||||
startLocationUpdates()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun loadData() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val (userDetails, tasks, currentLocation) =
|
||||
userDetailsAndTasksUseCase.fetchUserDetailsAndTasksWithLocation()
|
||||
val combinedResults = userDetailsAndTasksResultMapper.map(userDetails, tasks, currentLocation)
|
||||
// Update state with the combined result
|
||||
_resultState.value = combinedResults
|
||||
} catch (e: Exception) {
|
||||
// Handle errors and update UI accordingly
|
||||
_resultState.value = Result.Error(e.message ?: "An error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLocationUpdates() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
MIN_TIME_BETWEEN_UPDATES,
|
||||
MIN_DISTANCE_CHANGE_FOR_UPDATES,
|
||||
locationListener,
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
// Handle permission issues
|
||||
_resultState.value = Result.Error("Location permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val locationListener = LocationListener {
|
||||
// Handle location updates
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIN_TIME_BETWEEN_UPDATES: Long = 1000
|
||||
private const val MIN_DISTANCE_CHANGE_FOR_UPDATES: Float = 10f
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
data object Loading : Result()
|
||||
data class Success(val data: String) : Result()
|
||||
data class Error(val message: String) : Result()
|
||||
}
|
||||
}
|
||||
```
|
||||
## Aprofundando-se um pouco mais no tratamento com os Estados
|
||||
|
||||
Você pode notar o uso de estruturas de dados para gerencia estados da UI nos exemplos anteriores, como o `sealed class Result`.
|
||||
Todos esses resultados possíveis são imutáveis e este estado é exposto em um só lugar: `resultState`. Essa abordagem é um excelente
|
||||
remédio ao tratar um ViewModel doente que é difícil de testar porque teríamos um número finito de estados da View possíveis para
|
||||
validar e uma [Fonte Única de Verdade (SSOT)](https://developer.android.com/topic/architecture?hl=pt-br#single-source-of-truth).
|
||||
|
||||
Esta abordagem SSOT promove consistência e confiabilidade no gerenciamento de dados dentro do aplicativo. Ajuda a manter uma
|
||||
separação clara de preocupações, centralizando as operações de dados no ViewModel, que atua como um mediador entre a UI e as
|
||||
fontes de dados subjacentes. Essa arquitetura facilita a implementação do
|
||||
[Fluxo de Dados Unidirecional (UDF)](https://developer.android.com/topic/architecture?hl=pt-br#unidirectional-data-flow), onde os dados
|
||||
fluem em uma única direção — do ViewModel aos componentes da UI — evitando dependências circulares e garantindo um fluxo de dados
|
||||
previsível durante todo o ciclo de vida do aplicativo.
|
||||
|
||||

|
||||
|
||||
No diagrama acima podemos ver claramente como o UDF funciona com ViewModels:
|
||||
|
||||
1. O ViewModel mantém e expõe o estado da UI;
|
||||
2. A UI notifica o ViewModel sobre eventos (clique do botão, por exemplo);
|
||||
3. ViewModel trata os eventos, atualiza o estado e é consumido pela UI;
|
||||
4. Repita o fluxo.
|
||||
|
||||
Independente da arquitetura utilizada: MVVM, MVI, MVP, etc, concentre-se em como tornar seus dados de UI previsíveis, imutáveis e unidirecionais.
|
||||
Ao fazer isso, seu aplicativo será mais fácil de entender, testar e manter, resultando em melhores experiências do usuário.
|
||||
|
||||
## Conclusão
|
||||
|
||||
Criar ViewModels fáceis de testar é um desafio comum enfrentado pelos desenvolvedores, mas pode ser superado com a abordagem certa. Ao reconhecer
|
||||
os sintomas de um ViewModel mal projetado – como lógica pesada, dependências extensas, referências diretas a componentes do Framework Android e
|
||||
escopo excessivamente grande – podemos tomar medidas para resolver esses problemas e melhorar a qualidade geral e a capacidade de manutenção
|
||||
de nossa base de código.
|
||||
|
||||
Nesta jornada, exploramos os princípios básicos do design do ViewModel e por que aderir às práticas recomendadas é essencial. Ao aplicar conceitos
|
||||
como o Princípio de Responsabilidade Única(SRP), Separação de Preocupações(SOC), Fonte Única de Verdade(SSOT), Fluxo de Dados Unidirecional(UDF)
|
||||
e empregar princípios de arquitetura limpa, podemos refatorar nossos ViewModels para serem mais modulares, testáveis e resilientes.
|
||||
|
||||
E finalmente, seja paciente. Refatore seu código aos poucos com a ajuda dos testes existentes. Você não quer um ViewModel saudável com comportamentos errados.
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
---
|
||||
title: "Como acessar arquivos de teste de diferentes pacotes de teste (Unit e Instrumented) em Android"
|
||||
date: 2022-02-19
|
||||
lastmod: 2024-03-24
|
||||
description: "Aprenda como acessar arquivos de teste de diferentes pacotes de teste (Unit e Instrumented) em Android."
|
||||
featured_image: "/img/how-to-access-test-only-files-from-unit-and-instrumented-test-packages/featured_image.webp"
|
||||
draft: false
|
||||
---
|
||||
|
||||

|
||||
|
||||
## TL;DR
|
||||
|
||||
Crie um diretório chamado `testCommon` e adicione o trecho de código abaixo ao seu arquivo `build.gradle`.
|
||||
|
||||
```groovy
|
||||
android {
|
||||
...
|
||||
sourceSets {
|
||||
test { java.srcDirs += src/testCommon }
|
||||
androidTest { java.srcDirs += src/testCommon }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Você já esteve em uma situação em que precisa acessar arquivos utilitários tanto do pacote `test`
|
||||
quanto do `androidTest`? Se sim, este artigo pode ser útil para você.
|
||||
|
||||
Digamos que você tenha um arquivo chamado `Placeholders.kt` no qual você usa seus valores
|
||||
como **test doubles** para seus testes de unidade.
|
||||
|
||||
Nosso cenário de teste neste caso será o retorno `Success` do método login de uma
|
||||
classe chamada `LoginUseCaseImpl`.
|
||||
|
||||
Por exemplo:
|
||||
|
||||
```kotlin
|
||||
// test/Placeholders.kt
|
||||
object Placeholders {
|
||||
const val email = "johndoe@email.com"
|
||||
const val password = "strongpassword123"
|
||||
}
|
||||
|
||||
// test/LoginUseCaseImplTest.kt
|
||||
class LoginUseCaseImplTest {
|
||||
|
||||
private val loginUseCase = LoginUseCaseImpl()
|
||||
|
||||
@Test
|
||||
fun `when call login should return Success`() {
|
||||
// Arrange
|
||||
val email = Placeholders.email
|
||||
val password = Placeholders.password
|
||||
|
||||
// Act
|
||||
val result = loginUseCase.login(email, password)
|
||||
|
||||
// Assert
|
||||
assertEquals(Success, result)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Agora, precisamos escrever **Testes instrumentados** usando [Espresso](https://developer.android.com/training/testing/espresso/)
|
||||
para validar o fluxo de login completo. O cenário é: ao preencher os campos de e-mail e senha e tocar no botão de login
|
||||
então uma mensagem de sucesso será exibida.
|
||||
|
||||
```kotlin
|
||||
// androidTest/LoginScreenInstrumentedTest.kt
|
||||
class LoginScreenInstrumentedTest {
|
||||
@Test
|
||||
fun when_fill_the_email_and_password_fields_and_tap_the_login_button_then_a_success_message_will_display() {
|
||||
// Arrange
|
||||
val email = Placeholders.email
|
||||
val password = Placeholders.password
|
||||
|
||||
// When fill email and password fields
|
||||
onView(withId(R.id.email_field)).perform(ViewActions.typeText(email))
|
||||
onView(withId(R.id.password_field)).perform(ViewActions.typeText(password))
|
||||
|
||||
// And tap the login button
|
||||
onView(withId(R.id.login_button)).perform(ViewActions.click())
|
||||
|
||||
// Then a success message will display.
|
||||
onView(withId(R.id.success_message)).check(matches(isDisplayed()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Por padrão, o teste utilizando o Espresso estará dentro do pacote `androidTest`, mas nosso `Placeholders.kt`
|
||||
está disponível apenas dentro do pacote `test`, no qual nosso `LoginUseCaseImplTest` também está localizado.
|
||||
Portanto, o Teste Instrumentado acima não encontrará o `Placeholders.kt`.
|
||||
|
||||
Para permitir que ambos os testes acessem o mesmo arquivo, precisamos criar um novo pacote dentro do `src` que iremos colocar o `Placeholders.kt`.
|
||||
Neste exemplo, vamos nomeá-lo como `testCommon`. Depois disso, precisamos dizer ao gradle para considerar este novo pacote
|
||||
como um pacote `test` e `androidTest`. Colocaremos o código abaixo em nosso arquivo `build.gradle`:
|
||||
|
||||
```groovy
|
||||
android {
|
||||
...
|
||||
sourceSets {
|
||||
test { java.srcDirs += src/testCommon }
|
||||
androidTest { java.srcDirs += src/testCommon }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
É isso! Agora ambos os pacotes de teste poderão acessar nosso arquivo `Placeholders.kt`!
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
---
|
||||
title: "Como fazer o deploy de aplicativos React usando Github Actions + Rsync"
|
||||
date: 2022-10-15T15:30:22-03:00
|
||||
lastmod: 2024-03-24
|
||||
description: "Aprenda como fazer o deploy de aplicativos React usando Github Actions + Rsync"
|
||||
featured_image: "/img/how-to-deploy-react-applications-using-github-actions-+-rsync/featured_image.webp"
|
||||
draft: false
|
||||
---
|
||||
|
||||

|
||||
|
||||
Código fonte e aplicação em produção utilizado nesse post:
|
||||
- [rsync-deploy-react-app](https://github.com/leomurca/rsync-deploy-react-app);
|
||||
- [tutorials.leomurca.xyz/rsync-deploy-react-app](https://tutorials.leomurca.xyz/rsync-deploy-react-app/);
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Crie um usuário em seu servidor para fazer o deploy de sua aplicação:
|
||||
```shell
|
||||
$ useradd -s /bin/bash -d /home/tutorials -m tutorials
|
||||
$ su tutorials
|
||||
```
|
||||
|
||||
- Crie um diretório para copiar seus arquivos de produção:
|
||||
```shell
|
||||
$ mkdir rsync-deploy-react-app
|
||||
```
|
||||
|
||||
- Add your private tutorials's user ssh key to your [Action Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets).
|
||||
|
||||
- Adicione a chave ssh privada do usuário `tutorials` aos seus [Action Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets).
|
||||
|
||||
- Crie uma nova action no github para fazer o deploy da sua aplicação e cole o código abaixo:
|
||||
```yml
|
||||
# .github/workflows/deploy.yml
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SSH_KEY: ${{secrets.SSH_KEY}}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'npm'
|
||||
- run: mkdir ~/.ssh
|
||||
- run: 'echo "$SSH_KEY" >> ~/.ssh/id_rsa_tutorials'
|
||||
- run: chmod 400 ~/.ssh/id_rsa_tutorials
|
||||
- run: echo -e "Host tutorials\n\tUser tutorials\n\tHostname 45.76.5.44\n\tIdentityFile ~/.ssh/id_rsa_tutorials\n\tStrictHostKeyChecking No" >> ~/.ssh/config
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: rsync -avz --progress build/ tutorials:/home/tutorials/rsync-deploy-react-app --delete
|
||||
```
|
||||
|
||||
Preste atenção aos **Pré-requisitos**. É isso! Altere algum código, envie-o para o branch principal e veja a mágica acontecendo!
|
||||
|
||||
## Motivação
|
||||
|
||||
Estive trabalhando em minha tese de bacharelado em sistemas de informação, que é um aplicativo React simples, e estava lutando para encontrar uma maneira **simples, segura e rápida** de implantá-lo em meu próprio [VPS](https:/ /en.wikipedia.org/wiki/Virtual_private_server). No entanto, a maior parte do conteúdo que encontrei na internet envolve soluções **fantasiosas e complexas** como [docker](https://www.docker.com/) e [Kubernetes](https://kubernetes.io /) ou soluções **bloqueadas pelo fornecedor** como [Heroku](https://www.heroku.com/) e [Vercel](https://vercel.com/).
|
||||
|
||||
Reconheço que essas ferramentas têm suas vantagens, mas descobri que, para projetos de pequeno e médio porte, elas exigem mais esforço do que vale a pena manter. Tudo o que preciso fazer é criar o código e copiar os arquivos criados para o servidor. Então, [rsync](https://en.wikipedia.org/wiki/Rsync) chegou ao meu conhecimento.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
- Uma aplicação React existente;
|
||||
- Um servidor ou um serviço de hospedagem para fazer o deploy de sua aplicação;
|
||||
- Seu próprio domínio registrado;
|
||||
- [NGINX](https://www.nginx.com/) instalado em seu servidor;
|
||||
- Algum conhecimento sobre como registrar um domínio e criar um servidor em alguma plataforma de hospedagem (adicionarei artigos sobre isso no futuro).
|
||||
|
||||
## Aplicativo de demonstração a ser implantado
|
||||
|
||||
Criei um aplicativo de demonstração para fazer o deploy em meu servidor. Seu código-fonte está disponível em [rsync-deploy-react-app](https://github.com/leomurca/rsync-deploy-react-app).
|
||||
|
||||

|
||||
|
||||
## Setup do servidor
|
||||
|
||||
Para este tutorial, usarei meu domínio `leomurca.xyz` configurando um subdomínio para ele. Para ser mais específico, vou apontar `tutorial.leomurca.xyz` para o IP do meu **VPS**: `45.76.5.44`.
|
||||
|
||||
### Logar no servidor via SSH
|
||||
|
||||
```shell
|
||||
$ ssh root@45.76.5.44
|
||||
```
|
||||
|
||||
### Crie um usuário para gerenciar sua aplicação
|
||||
|
||||
Para evitar que nosso pipeline tenha acesso root ao seu servidor, criarei um usuário para gerenciar deploys chamado `tutorials`:
|
||||
|
||||
```shell
|
||||
$ useradd -s /bin/bash -d /home/tutorials -m tutorials
|
||||
```
|
||||
|
||||
Depois disso, logue como o usuário criado:
|
||||
|
||||
```shell
|
||||
$ su tutorials
|
||||
```
|
||||
|
||||
Como criei o usuário chamado `tutorials`, este usuário irá hospedar vários tutoriais, então para isolarmos nossa aplicação, crie uma pasta específica para abrigar nossos arquivos de build:
|
||||
|
||||
```shell
|
||||
$ mkdir rsync-deploy-react-app
|
||||
```
|
||||
|
||||
## Setup da Github Action
|
||||
|
||||
Agora vamos criar o `.github/workflows/deploy.yml` para definir as etapas do pipeline. Primeiro, adicione uma label para o fluxo de trabalho e quando ele deve ser acionado:
|
||||
|
||||
```yml
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
```
|
||||
|
||||
Acima, o workflow será acionado toda vez que um novo código for **pushed** ou **merged** à branch `main` (isso também acontece para Pull Requests mergeadas à branch principal).
|
||||
|
||||
Em seguida, descreva um novo workflow que vamos nomear como `build-and-deploy` para lidar com todas as etapas para fazer o build e o deploy de nossa aplicação:
|
||||
|
||||
```yml
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SSH_KEY: ${{secrets.SSH_KEY}}
|
||||
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
A `SSH_KEY: ${{secrets.SSH_KEY}}` faz referência a [Github Secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) que é permitido fazer login em nosso servidor. É importante mencionar que devemos adicionar o secrete às configurações do nosso repositório.
|
||||
|
||||
Além disso, para evitar problemas ao autenticar em seu servidor usando ssh, **use chaves geradas por RSA para autenticar em vez de chaves Ed25519**. Para mais detalhes sobre isso, verifique este documento sobre [como gerar uma nova chave SSH](https://docs.github.com/pt/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent).
|
||||
|
||||
Depois, vamos começar a definir os passos reais a serem executados:
|
||||
|
||||
```yml
|
||||
...
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'npm'
|
||||
...
|
||||
```
|
||||
|
||||
Essas primeiras etapas basicamente definirão qual versão do [NodeJS](https://nodejs.org/en/) será usada em nosso pipeline.
|
||||
|
||||
E agora, um passo muito importante é adicionar os comandos que realmente serão executados em nosso workflow, preste atenção neles:
|
||||
|
||||
```yml
|
||||
...
|
||||
- run: mkdir ~/.ssh
|
||||
- run: 'echo "$SSH_KEY" >> ~/.ssh/id_rsa_tutorials'
|
||||
- run: chmod 400 ~/.ssh/id_rsa_tutorials
|
||||
- run: echo -e "Host tutorials\n\tUser tutorials\n\tHostname 45.76.5.44\n\tIdentityFile ~/.ssh/id_rsa_tutorials\n\tStrictHostKeyChecking No" >> ~/.ssh/config
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
...
|
||||
```
|
||||
|
||||
As configurações acima nós basicamente:
|
||||
- Copiamos a `SSH_KEY` para um arquivo;
|
||||
- Criamos uma configuração ssh para nosso servidor usando a chave criada anteriormente;
|
||||
- Baixamos as dependências do aplicativo e gere os arquivos de compilação para serem copiados para o nosso servidor.
|
||||
|
||||
Para tornar as configurações do ssh mais legíveis, verifique o trecho de código abaixo:
|
||||
```shell
|
||||
Host tutorials
|
||||
User tutorials
|
||||
Hostname 45.76.5.44
|
||||
IdentityFile ~/.ssh/id_rsa_tutorials
|
||||
StrictHostKeyChecking No
|
||||
```
|
||||
|
||||
### Usando rsync para fazer o deploy no servidor
|
||||
|
||||
E, finalmente, adicione o comando rsync para sincronizar os arquivos da pasta `build/` para o nosso servidor:
|
||||
|
||||
```yml
|
||||
...
|
||||
- run: rsync -avz --progress build/ tutorials:/home/tutorials/rsync-deploy-react-app --delete
|
||||
...
|
||||
```
|
||||
|
||||
O significado de cada flag utilizada são:
|
||||
- `-a`: É uma maneira rápida de dizer que você quer recursão e quer preservar quase tudo (com -H sendo uma omissão notável);
|
||||
- `-v` (`-verbose`): Esta opção aumenta a quantidade de informações que o daemon registra durante sua fase de inicialização.
|
||||
- `-z` (`-compress`): comprime os dados do arquivo à medida que são enviados para a máquina de destino, o que reduz a quantidade de dados sendo transmitidos -- algo que é útil em uma conexão lenta.
|
||||
- `--progress`: Esta opção diz ao rsync para imprimir informações mostrando o progresso da transferência. Isso dá a um usuário entediado algo para assistir.
|
||||
- `--delete`: Diz ao rsync para excluir arquivos estranhos do lado receptor (aqueles que não estão no lado remetente), mas apenas para os diretórios que estão sendo sincronizados.
|
||||
|
||||
Para obter mais detalhes sobre todas as opções do `rsync`, consulte sua [man page](https://linux.die.net/man/1/rsync).
|
||||
|
||||
### Arquivo completo `.github/workflows/deploy.yml`
|
||||
|
||||
```yml
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SSH_KEY: ${{secrets.SSH_KEY}}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'npm'
|
||||
- run: mkdir ~/.ssh
|
||||
- run: 'echo "$SSH_KEY" >> ~/.ssh/id_rsa_tutorials'
|
||||
- run: chmod 400 ~/.ssh/id_rsa_tutorials
|
||||
- run: echo -e "Host tutorials\n\tUser tutorials\n\tHostname 45.76.5.44\n\tIdentityFile ~/.ssh/id_rsa_tutorials\n\tStrictHostKeyChecking No" >> ~/.ssh/config
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: rsync -avz --progress build/ tutorials:/home/tutorials/rsync-deploy-react-app --delete
|
||||
```
|
||||
|
||||
É isso! Altere algum código, envie-o para o branch principal e veja a mágica acontecendo!
|
||||
|
||||

|
||||
|
||||
Além disso, se você quiser obter mais detalhes sobre as etapas da ação, verifique as [ações executadas](https://github.com/leomurca/rsync-deploy-react-app/actions) durante este artigo.
|
||||
|
||||
**Simples, rápido e seguro**, esses são os principais benefícios de usar o fluxo de trabalho mencionado neste tutorial. É realmente um alívio ter esse tipo de ferramenta no meio de tantas soluções inchadas.
|
||||
|
||||
Se você tiver alguma dúvida ou assunto para falar, por favor, [fale comigo](/contato)!
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
---
|
||||
title: "Como fazer push para múltiplos repositórios no git com apenas 1 comando"
|
||||
date: 2022-10-11T16:51:53-03:00
|
||||
lastmod: 2024-03-24
|
||||
description: "Aprenda como fazer push para múltiplos repositórios no git com apenas 1 comando (terminal)."
|
||||
featured_image: "/img/how-to-push-to-multiple-git-remotes-with-one-command/featured_image.webp"
|
||||
draft: false
|
||||
---
|
||||
|
||||

|
||||
|
||||
Repositório utilizados neste tutorial:
|
||||
- [gh-remote](https://github.com/leomurca/gh-remote);
|
||||
- [gl-remote](https://gitlab.com/leomurca/gl-remote.git).
|
||||
|
||||
## TL;DR
|
||||
- Clone repositório primário ([gh-remote](https://github.com/leomurca/gh-remote));
|
||||
|
||||
```shell
|
||||
$ git clone git@github.com:leomurca/gh-remote.git
|
||||
```
|
||||
|
||||
- Adicione o remote do repositório secundário ([gl-remote](https://gitlab.com/leomurca/gl-remote.git)) ao diretório clonado;
|
||||
|
||||
```shell
|
||||
$ git remote add gitlab git@gitlab.com:leomurca/gl-remote.git
|
||||
```
|
||||
|
||||
- Adicione um terceiro remote que será utilizado para fazer o push para todos os remotes ao mesmo tempo. Por conveniência, vamos nomeá-lo como `all`. Além disso, nomeie sua url como `fetch-not-supported`;
|
||||
|
||||
```shell
|
||||
$ git remote add all fetch-not-supported
|
||||
```
|
||||
|
||||
- Adicione os remotes que você deseja que seu código seja enviado quando `git push all <BRANCH>` for executado;
|
||||
|
||||
```shell
|
||||
$ git remote set-url --add --push all git@gitlab.com:leomurca/gl-remote.git
|
||||
$ git remote set-url --add --push all git@github.com:leomurca/gh-remote.git
|
||||
```
|
||||
|
||||
- E, finalmente, para testar se está funcionando, altere algum código localmente, execute o comando abaixo e verifique seus dois remotes (esteja ciente da branch que você está usando. Em nosso exemplo, estamos usando o branch `main`).
|
||||
|
||||
```shell
|
||||
$ git push all main
|
||||
Enumerating objects: 5, done.
|
||||
Counting objects: 100% (5/5), done.
|
||||
Writing objects: 100% (3/3), 275 bytes | 91.00 KiB/s, done.
|
||||
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||
To gitlab.com:leomurca/gl-remote.git
|
||||
584aa2b..1a17aa1 main -> main
|
||||
Enumerating objects: 8, done.
|
||||
Counting objects: 100% (8/8), done.
|
||||
Delta compression using up to 10 threads
|
||||
Compressing objects: 100% (2/2), done.
|
||||
Writing objects: 100% (6/6), 512 bytes | 85.00 KiB/s, done.
|
||||
Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||
To github.com:leomurca/gh-remote.git
|
||||
7e0fb66..1a17aa1 main -> main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Introdução
|
||||
|
||||
Este tutorial é bem direto sobre como simplificar a sincronização entre vários remotes git, recomendo salvá-lo em seus favoritos caso esqueça alguma etapa.
|
||||
|
||||
## Por que fazer o psuh para múltiplos remotes git?
|
||||
|
||||
O principal caso de uso para usar vários remotes git é sincronizar suas alterações entre todos os espelhos usados como redundância para seu projeto. Se esse for o seu caso (ou mesmo se não for, mas você quer aprender algo novo), vamos mergulhar de cabeça.
|
||||
|
||||
## Adicionando múltiplos remotes
|
||||
|
||||
Primeiro de tudo, você precisa ter o repositório git primário clonado em sua máquina local. Você também pode começar criando um repositório local e, em seguida, enviar para um remoto, mas para este tutorial, vamos simplificar. Então, vamos clonar nosso repositório primário, que no caso é o [gh-remote](https://github.com/leomurca/gh-remote).
|
||||
|
||||
### Clonando o repositório (com o remote primário)
|
||||
|
||||
```shell
|
||||
$ git clone git@github.com:leomurca/gh-remote.git
|
||||
```
|
||||
|
||||
Depois de cloná-lo, verifique o remote padrão atribuído a ele.
|
||||
|
||||
```shell
|
||||
$ git remote -v
|
||||
origin git@github.com:leomurca/gh-remote.git (fetch)
|
||||
origin git@github.com:leomurca/gh-remote.git (push)
|
||||
```
|
||||
|
||||
### Adicionando um segundo remote
|
||||
|
||||
You can notice that the default remote name assigned to the primary repo was `origin`, but you can also change it in the future. Right, now let's just add the second remote ([gl-remote](https://gitlab.com/leomurca/gl-remote.git)) to the cloned folder and then list all the remotes afterwards;
|
||||
|
||||
Você pode notar que o nome padrão do remote atribuído ao repositório primário era `origin`, mas também pode alterá-lo no futuro. Certo, agora vamos apenas adicionar o segundo remote ([gl-remote](https://gitlab.com/leomurca/gl-remote.git)) na pasta clonada e depois listar todos os controles remotos;
|
||||
|
||||
```shell
|
||||
$ git remote add gitlab git@gitlab.com:leomurca/gl-remote.git
|
||||
$ git remote -v
|
||||
gitlab git@gitlab.com:leomurca/gl-remote.git (fetch)
|
||||
gitlab git@gitlab.com:leomurca/gl-remote.git (push)
|
||||
origin git@github.com:leomurca/gh-remote.git (fetch)
|
||||
origin git@github.com:leomurca/gh-remote.git (push)
|
||||
```
|
||||
|
||||
### Adicionando um terceiro remote (somente push)
|
||||
Adicionaremos um terceiro remote que será usado para enviar para todos os remotes ao mesmo tempo. Por conveniência, vamos nomeá-lo como `all`. Além disso, como url, usaremos o valor `fetch-not-supported`. Depois disso, liste os remotes.
|
||||
|
||||
```shell
|
||||
$ git remote add all fetch-not-supported
|
||||
$ git remote -v
|
||||
all fetch-not-supported (fetch)
|
||||
all fetch-not-supported (push)
|
||||
gitlab git@gitlab.com:leomurca/gl-remote.git (fetch)
|
||||
gitlab git@gitlab.com:leomurca/gl-remote.git (push)
|
||||
origin git@github.com:leomurca/gh-remote.git (fetch)
|
||||
origin git@github.com:leomurca/gh-remote.git (push)
|
||||
```
|
||||
|
||||
E, finalmente, adicionaremos os remotes que queremos que nosso código seja enviado quando `git push all <BRANCH>` for executado;
|
||||
|
||||
```shell
|
||||
$ git remote set-url --add --push all git@gitlab.com:leomurca/gl-remote.git
|
||||
$ git remote set-url --add --push all git@github.com:leomurca/gh-remote.git
|
||||
```
|
||||
|
||||
## Liste todos os remotes
|
||||
|
||||
Vamos ver o resultado final listando todos os remotes adicionados anteriormente.
|
||||
|
||||
```shell
|
||||
$ git remote -v
|
||||
all fetch-not-supported (fetch)
|
||||
all git@gitlab.com:leomurca/gl-remote.git (push)
|
||||
all git@github.com:leomurca/gh-remote.git (push)
|
||||
gitlab git@gitlab.com:leomurca/gl-remote.git (fetch)
|
||||
gitlab git@gitlab.com:leomurca/gl-remote.git (push)
|
||||
origin git@github.com:leomurca/gh-remote.git (fetch)
|
||||
origin git@github.com:leomurca/gh-remote.git (push)
|
||||
```
|
||||
|
||||
## Fazendo push para múltiplos remotes
|
||||
|
||||
And now that everything is set up, let's test if it is working: change some code locally, run the command below and check your both remotes (Be aware of the branch that you are using. In our example, we are using the `main` branch).
|
||||
|
||||
E agora que tudo está configurado, vamos testar se está funcionando: altere algum código localmente, execute o comando abaixo e verifique seus dois remotes (Fique atento à branch que você está usando. Em nosso exemplo, estamos usando a branch main `main `).
|
||||
|
||||
```shell
|
||||
$ git push all main
|
||||
Enumerating objects: 5, done.
|
||||
Counting objects: 100% (5/5), done.
|
||||
Writing objects: 100% (3/3), 275 bytes | 91.00 KiB/s, done.
|
||||
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||
To gitlab.com:leomurca/gl-remote.git
|
||||
584aa2b..1a17aa1 main -> main
|
||||
Enumerating objects: 8, done.
|
||||
Counting objects: 100% (8/8), done.
|
||||
Delta compression using up to 10 threads
|
||||
Compressing objects: 100% (2/2), done.
|
||||
Writing objects: 100% (6/6), 512 bytes | 85.00 KiB/s, done.
|
||||
Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||
To github.com:leomurca/gh-remote.git
|
||||
7e0fb66..1a17aa1 main -> main
|
||||
```
|
||||
|
||||
Isso é tudo! Aproveite sua nova configuração git.
|
||||
|
||||
## Extra
|
||||
|
||||
Quer fazer um fetch de vários remotes? Basta executar `git fetch --all`.
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
---
|
||||
title: "Melhores contextos para utilizar WebViews no desenvolvimento Android"
|
||||
date: 2023-02-14T19:58:00-03:00
|
||||
featured_image: "/img/best-use-cases-for-webViews-in-android-development.webp"
|
||||
draft: true
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
WebViews in native Android development can be used for a variety of purposes, but some of the best use cases are:
|
||||
|
||||
1. Provide information in your app that you might need to update, such as end-user agreement or a user guide;
|
||||
|
||||
2. Display web pages such as news articles, social media feeds, and web-based applications, within your app.
|
||||
|
||||
3. Authentication: If your app requires authentication with an external service, you can use WebViews to display the login page and handle the authentication flow.
|
||||
|
||||
(INVALID) 4. Payment gateways: You can use WebViews to display payment gateways within your app. This allows you to accept payments from within your app without the need to redirect users to a separate website.
|
||||
|
||||
4. HTML5 games: If your app includes HTML5 games, you can use WebViews to display them within the app.
|
||||
|
||||
5. Advertising: You can use WebViews to display advertisements within your app. This allows you to monetize your app by displaying ads without the need to leave the app.
|
||||
|
||||
6. Custom UI elements: You can use WebViews to create custom UI elements within your app, such as custom buttons or menus.
|
||||
|
||||
7. Chatbots: You can use WebViews to display chatbots within your app. This allows users to interact with chatbots without leaving the app.
|
||||
|
||||
8. Implement a MVP: If you want to validate an idea but you don't have enough android engineers to implementa all natively, just port your existing web app to Android webview.
|
||||
|
||||
Overall, WebViews can be a useful tool for integrating web-based content and services into your native Android app.
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
---
|
||||
title: "Visualizador de Bordados"
|
||||
description: "Uma ferramenta online gratuita para visualizar arquivos de bordado escrita com Svelte."
|
||||
lead_video: "/img/embroidery-viewer/lead-video.mp4"
|
||||
featured_image: "/img/embroidery-viewer/screenshot.png"
|
||||
status: "Live"
|
||||
draft: false
|
||||
---
|
||||
|
||||
## Sobre o Projeto
|
||||
|
||||
**Embroidery Viewer** é uma aplicação web leve, focada em privacidade e de código aberto que permite visualizar arquivos de bordado instantaneamente no navegador. Ele oferece suporte a uma ampla variedade de formatos de arquivos, incluindo `.PES`, `.DST`, `.EXP`, `.JEF`, entre outros.
|
||||
|
||||
A ferramenta funciona inteiramente no lado do cliente, o que significa que os arquivos nunca saem do dispositivo do usuário, garantindo total privacidade. Com uma interface limpa e intuitiva, permite visualizar múltiplos arquivos de bordado ao mesmo tempo, facilitando a comparação e revisão dos designs.
|
||||
|
||||
### ✨ Funcionalidades
|
||||
|
||||
- 📂 Suporte a Múltiplos Formatos: `.DST`, `.PES`, `.JEF`, `.EXP`, `.VP3` e outros
|
||||
- ⚡ Visualização Instantânea: Veja seus arquivos de bordado renderizados como imagens
|
||||
- 🧷 Múltiplos Arquivos Simultaneamente: Faça upload de vários designs e veja lado a lado
|
||||
- 🔒 Sem Upload para o Servidor: Seus arquivos permanecem privados – todo o processamento ocorre localmente
|
||||
- ⬇️ Baixe como Imagem: Salve cada visualização de design como PNG
|
||||
- 💸 Rápido e Gratuito: Sem instalação, sem cadastro – é só abrir e usar
|
||||
|
||||
## 🔧 Tecnologias Utilizadas
|
||||
|
||||
- **Frontend:** Svelte, HTML5, CSS3
|
||||
- **Renderização:** HTML Canvas e SVG
|
||||
- **Hospedagem:** VPS próprio com Nginx
|
||||
- **Outros:** Git para controle de versão, infraestrutura enxuta
|
||||
|
||||
## 🤝 Código Aberto & Contribuições
|
||||
|
||||
Embroidery Viewer é um projeto de **código aberto**. Seja você desenvolvedor, designer ou entusiasta do bordado, [sua contribuição é bem-vinda](https://git.leomurca.xyz/leomurca/embroidery-viewer).
|
||||
|
||||
## 🚀 Desafios & Soluções
|
||||
|
||||
- **Leitura de Arquivos Binários no Navegador:** Implementado parser de arquivos binários complexos diretamente no lado do cliente.
|
||||
- **Renderização de Pontos:** Conversão dos dados de pontos em caminhos SVG ou Canvas para visualização precisa.
|
||||
- **Desempenho:** Otimizado para lidar com múltiplos arquivos com uso mínimo de memória e processamento rápido.
|
||||
|
||||
## 📈 Impacto
|
||||
|
||||
- Auxilia hobistas e profissionais de bordado a visualizar arquivos sem softwares caros.
|
||||
- Atraiu tráfego orgânico de comunidades de bordado.
|
||||
- Destaque em diversos fóruns e comunidades do nicho.
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- 🌐 [Visite o site](https://embroideryviewer.xyz)
|
||||
- 👨💻 [Código-fonte](https://git.leomurca.xyz/leomurca/embroidery-viewer)
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
---
|
||||
title: "Entrepreneurship Alagoas"
|
||||
description: "Landing page apresentando programas de startups de Alagoas com financiamento e mentoria."
|
||||
lead_video: "/img/entrepreneurship-alagoas/lead-video.mp4"
|
||||
featured_image: "/img/entrepreneurship-alagoas/screenshot.png"
|
||||
status: "Live"
|
||||
draft: false
|
||||
---
|
||||
|
||||
## Sobre o Projeto
|
||||
|
||||
**Empreendedorismo SECTI Alagoas** é uma landing page desenvolvida como projeto freelance para divulgar os programas **Lagoon Startup** e **VAI Startup** — iniciativas públicas que apoiam empreendedores em Alagoas por meio de financiamento, mentoria e metodologia estruturada.
|
||||
|
||||
O site foi desenvolvido com **SvelteKit**, com foco em performance, responsividade e clareza na comunicação das informações.
|
||||
|
||||
### ✨ Funcionalidades
|
||||
|
||||
- 🌱 **Divulgação de Programas**: Apresentação completa dos programas **Lagoon Startup** e **VAI Startup**, com informações sobre benefícios, cronogramas e requisitos
|
||||
- ✅ **Layout Responsivo**: Otimizado para navegação em dispositivos móveis, tablets e desktops
|
||||
- 🌐 **SEO e Performance**: Estrutura semântica e carregamento rápido para melhor desempenho e indexação
|
||||
- 🎯 **Chamadas à Ação**: Destaques para inscrições, objetivos e etapas do processo
|
||||
|
||||
### 🔧 Tecnologias Utilizadas
|
||||
|
||||
- **Frontend:** SvelteKit, HTML5, CSS3
|
||||
- **Hospedagem:** VPS próprio
|
||||
- **Ferramentas:** Git para versionamento; CI/CD para automação de deploy
|
||||
|
||||
### 🤝 Meu Papel
|
||||
|
||||
Como desenvolvedor full-stack freelance, fui responsável por:
|
||||
|
||||
1. Levantamento de requisitos e alinhamento com a identidade da SECTI
|
||||
2. Criação de um layout responsivo e objetivo
|
||||
3. Implementação das seções com conteúdo sobre os programas
|
||||
4. Otimização para SEO, desempenho e acessibilidade
|
||||
5. Configuração do ambiente de deploy automatizado (CI/CD) em VPS
|
||||
|
||||
### 📈 Impacto
|
||||
|
||||
- Auxiliou a SECTI na promoção profissional de seus programas de incentivo ao empreendedorismo
|
||||
- Tornou mais acessíveis as informações para startups e empreendedores locais
|
||||
- Reforçou a presença digital de políticas públicas de fomento à inovação em Alagoas
|
||||
|
||||
### 🔗 Links
|
||||
|
||||
- 🌐 [Acessar página](https://empreendedorismo.secti.al.gov.br/)
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
---
|
||||
title: "Tecnologias Inovadoras para a Reparação (TIR)"
|
||||
description: "Landing page em SvelteKit para divulgar edital de fomento a projetos tecnológicos."
|
||||
lead_video: "/img/itr/lead-video.mp4"
|
||||
featured_image: "/img/itr/screenshot.png"
|
||||
status: "Archived"
|
||||
draft: false
|
||||
---
|
||||
|
||||
## Sobre o Projeto
|
||||
|
||||
**TIR - Tecnologias de Impacto Relevante** é uma landing page desenvolvida com **SvelteKit** para promover um edital voltado ao apoio de projetos de desenvolvimento tecnológico no Brasil. O site apresenta de forma clara as etapas do edital, benefícios oferecidos e critérios de seleção, com foco em conversão e performance.
|
||||
|
||||
A página faz uso de **renderização server-side (SSR)** para melhorar a indexação por motores de busca e otimizar o tempo de carregamento, mesmo em conexões lentas.
|
||||
|
||||
### ✨ Funcionalidades
|
||||
|
||||
- 📄 **Apresentação do Edital**: Informações completas sobre capacitações, critérios, cronograma e benefícios
|
||||
- 🎯 **Foco em Conversão**: Estrutura clara com destaque para botões de inscrição e chamadas à ação
|
||||
- ⚡ **SSR com SvelteKit**: Renderização server-side para melhor SEO e desempenho
|
||||
- ✅ **Design Responsivo**: Visual limpo e compatível com diferentes tamanhos de tela
|
||||
- 🌐 **Otimização SEO**: Marcações semânticas e carregamento rápido
|
||||
|
||||
### 🔧 Tecnologias Utilizadas
|
||||
|
||||
- **Frontend:** SvelteKit (com SSR), HTML5, CSS3
|
||||
- **Hospedagem:** VPS próprio
|
||||
- **Ferramentas:** Git para versionamento; CI/CD para deploy automatizado
|
||||
|
||||
### 🤝 Meu Papel
|
||||
|
||||
Como desenvolvedor full-stack freelance, fui responsável por:
|
||||
|
||||
1. Criar o layout e o design responsivo com base na identidade do programa
|
||||
2. Implementar a estrutura do site com foco em performance e legibilidade
|
||||
3. Configurar a renderização server-side com SvelteKit
|
||||
4. Otimizar o conteúdo para SEO e acessibilidade
|
||||
5. Automatizar o deploy via CI/CD
|
||||
|
||||
### 📈 Impacto
|
||||
|
||||
- Facilitou o acesso às informações do edital TIR para potenciais proponentes
|
||||
- Aumentou a visibilidade do programa junto ao público-alvo por meio de SEO eficiente
|
||||
- Contribuiu para um processo seletivo mais acessível e transparente
|
||||
|
||||
### 🔗 Links
|
||||
|
||||
- 🌐 [Ver demo](https://projects.leomurca.xyz/tir/)
|
||||
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
---
|
||||
title: "Lacine"
|
||||
description: "Landing page desenvolvida com SvelteKit para divulgar o Lacine, um laboratório criativo para cineastas."
|
||||
lead_video: "/img/lacine/lead-video.mp4"
|
||||
featured_image: "/img/lacine/screenshot.png"
|
||||
status: "Archived"
|
||||
draft: false
|
||||
---
|
||||
|
||||
## Sobre o Projeto
|
||||
|
||||
**Lacine** é um site personalizado desenvolvido como um projeto freelance para um cliente do setor de cinema independente. O site foi criado para divulgar uma programação cultural com uma estética limpa e uma experiência centrada no usuário.
|
||||
|
||||
Construído do zero com foco em responsividade e desempenho, ele entrega conteúdo rápido, acessível e que funciona perfeitamente em qualquer dispositivo.
|
||||
|
||||
### ✨ Funcionalidades
|
||||
|
||||
- 🎬 **Listagem de Eventos**: Apresenta uma estrutura orgânica com datas e horários de cada sessão
|
||||
- ✅ **Design Responsivo**: Otimizado para mobile, tablet e desktop
|
||||
- 🌐 **SEO e Performance Otimizados**: Estrutura semântica e carregamento rápido
|
||||
- 💼 **Identidade Visual Personalizada**: Layout adaptado à identidade do cliente
|
||||
- 🎨 **Narrativa Visual**: Apresentação cinematográfica que destaca clima, agenda e chamadas para ação
|
||||
|
||||
## 🔧 Tecnologias Utilizadas
|
||||
|
||||
- **Frontend:** SvelteKit, HTML5, CSS3
|
||||
- **Hospedagem:** VPS próprio
|
||||
- **Ferramentas:** Git para controle de versão, CI/CD para deploy
|
||||
|
||||
## 🤝 Meu Papel
|
||||
|
||||
Como desenvolvedor full-stack freelance, fui responsável por:
|
||||
|
||||
1. Levantar e refinar os requisitos do projeto junto ao cliente
|
||||
2. Criar uma interface elegante e responsiva
|
||||
3. Implementar funcionalidades essenciais (agenda e detalhes dos eventos)
|
||||
4. Otimizar o site para desempenho e SEO
|
||||
5. Configurar a infraestrutura e realizar o deploy
|
||||
6. Com um cronograma apertado, desenvolvi tudo em ritmo acelerado
|
||||
|
||||
## 📈 Impacto
|
||||
|
||||
- Ajudei o cliente a estabelecer uma presença online profissional com controle total do conteúdo
|
||||
- O produto final se destaca pela clareza, estética e usabilidade dentro de um nicho específico
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- 🌐 [Ver demo](https://projects.leomurca.xyz/lacine/)
|
||||
39
ecosystem.config.cjs
Normal file
39
ecosystem.config.cjs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'leomurca-prod',
|
||||
script: './build/index.js',
|
||||
time: true,
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
max_restarts: 50,
|
||||
watch: false,
|
||||
max_memory_restart: '1G',
|
||||
env: {
|
||||
PUBLIC_APP_ENV: 'production',
|
||||
NODE_ENV: 'production',
|
||||
PORT: 7296,
|
||||
},
|
||||
},
|
||||
],
|
||||
deploy: {
|
||||
production: {
|
||||
user: 'deployer',
|
||||
host: '45.76.5.44',
|
||||
key: 'deploy.key',
|
||||
ref: 'origin/master',
|
||||
repo: 'git@git.leomurca.xyz:leomurca/leomurca.git',
|
||||
path: '/home/deployer/prod-leomurca',
|
||||
'pre-deploy':
|
||||
'rm -rf node_modules build .svelte-kit && npm ci && PUBLIC_APP_ENV=production npm run build',
|
||||
'post-deploy':
|
||||
'pm2 startOrReload ecosystem.config.cjs --only leomurca-prod --env production && pm2 save',
|
||||
|
||||
env: {
|
||||
PUBLIC_APP_ENV: 'production',
|
||||
NODE_ENV: 'production',
|
||||
PORT: 7296,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
32
eslint.config.js
Normal file
32
eslint.config.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import prettier from 'eslint-config-prettier';
|
||||
import path from 'node:path';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import js from '@eslint/js';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import globals from 'globals';
|
||||
import svelteConfig from './svelte.config.js';
|
||||
|
||||
const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
|
||||
|
||||
export default defineConfig([
|
||||
includeIgnoreFile(gitignorePath),
|
||||
js.configs.recommended,
|
||||
svelte.configs.recommended,
|
||||
prettier,
|
||||
svelte.configs.prettier,
|
||||
{
|
||||
languageOptions: { globals: { ...globals.browser, ...globals.node } },
|
||||
},
|
||||
|
||||
{
|
||||
files: ['**/*.svelte', '**/*.svelte.js'],
|
||||
languageOptions: { parserOptions: { svelteConfig } },
|
||||
},
|
||||
|
||||
{
|
||||
// Override or add rule settings here, such as:
|
||||
// 'svelte/button-has-type': 'error'
|
||||
rules: {},
|
||||
},
|
||||
]);
|
||||
64
i18n/en.toml
64
i18n/en.toml
|
|
@ -1,64 +0,0 @@
|
|||
[home]
|
||||
other = 'Home'
|
||||
[home-page-description]
|
||||
other="Leonardo Murça Webpage"
|
||||
[my-name-is]
|
||||
other="Hi there! 👋 I'm"
|
||||
[home-introduction]
|
||||
other="I am a passionate software engineer with {{ .years }} years of working experience. My expertise includes advanced skills in Jetpack Compose, Bluetooth Low Energy (BLE), Android Streaming Engineering, UI testing, and Coroutines, enabling me to deliver high-performance Android solutions. I have a proven track record of successfully mentoring and training new interns, contributing to team growth and skill development. I also have some good knowledge in Web Development. :)"
|
||||
[home-position]
|
||||
other="Android Engineer at"
|
||||
[my-projects]
|
||||
other="Projects"
|
||||
[my-projects-description]
|
||||
other="Freelance and side projects."
|
||||
[posted-on]
|
||||
other="Posted on "
|
||||
[last-update]
|
||||
other="Last update "
|
||||
[posts]
|
||||
other="Posts"
|
||||
[post-date-format]
|
||||
other="Jan 2, 2006"
|
||||
[single-post-date-format]
|
||||
other="Jan 2, 2006"
|
||||
[embroidery-viewer-title]
|
||||
other="Embroidery Viewer"
|
||||
[embroidery-viewer-description]
|
||||
other="A free online tool to view embroidery files built with Svelte."
|
||||
[lacine-title]
|
||||
other="Lacine"
|
||||
[lacine-description]
|
||||
other="Landing page built with SvelteKit to promote Lacine, a creative lab for filmmakers."
|
||||
[entrepreneurship-alagoas-title]
|
||||
other="Entrepreneurship Alagoas"
|
||||
[entrepreneurship-alagoas-description]
|
||||
other="Landing page built with SvelteKit to promote Lagoon Startup and VAI Startup, two programs supporting entrepreneurs in Alagoas with funding, mentorship, and methodology."
|
||||
[itr-title]
|
||||
other="Innovative Technologies for Repair (ITR)"
|
||||
[itr-description]
|
||||
other="Landing page built with SvelteKit to promote a public call for tech development projects in Brazil, offering training and funding support."
|
||||
[iebtplus-title]
|
||||
other="IEBT+"
|
||||
[iebtplus-description]
|
||||
other="Platform built with SvelteKit to connect startups with exclusive partner benefits through IEBT Plus. I also developed an admin application with Strapi to manage partner data."
|
||||
[vumbora-startups-title]
|
||||
other="Vum Bora Startups"
|
||||
[vumbora-startups-description]
|
||||
other="Landing page and a CMS for a project that capacitates, axcelerates and reward business with growth potential."
|
||||
[copyright]
|
||||
other="Leonardo Murça All rights reserved"
|
||||
[rss-description-recent-content]
|
||||
other="Recent content"
|
||||
[rss-in]
|
||||
other="in"
|
||||
[rss-on]
|
||||
other="on"
|
||||
[all-rights-reserved]
|
||||
other="All rights reserved"
|
||||
[projectStatus_live]
|
||||
other = "Live"
|
||||
[projectStatus_archived]
|
||||
other = "Archived"
|
||||
[my-resume]
|
||||
other = "My resume"
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
[home]
|
||||
other = 'Página Inicial'
|
||||
[home-page-description]
|
||||
other="Webpage de Leonardo Murça"
|
||||
[my-name-is]
|
||||
other="Olá, 👋 sou "
|
||||
[home-introduction]
|
||||
other="Sou um engenheiro de software Android apaixonado, com {{ .years }} anos de experiência na construção de aplicações seguras e confiáveis. Minha expertise inclui habilidades avançadas em Jetpack Compose, Bluetooth Low Energy (BLE), Android Media Streaming, testes de UI e Coroutines, permitindo-me entregar soluções Android de alto desempenho. Tenho um histórico comprovado de sucesso em mentorar e treinar novos estagiários, contribuindo para o crescimento da equipe e desenvolvimento de habilidades. Também tenho bons conhecimentos em Desenvolvimento Web. :)"
|
||||
[home-position]
|
||||
other="Desenvolvedor Android na"
|
||||
[my-projects]
|
||||
other="Projetos"
|
||||
[my-projects-description]
|
||||
other="Freelance e projetos paralelos."
|
||||
[posted-on]
|
||||
other="Publicado em "
|
||||
[last-update]
|
||||
other="Última atualização "
|
||||
[posts]
|
||||
other="Publicações"
|
||||
[post-date-format]
|
||||
other="2 Jan 2006"
|
||||
[single-post-date-format]
|
||||
other="2 de January, 2006"
|
||||
[embroidery-viewer-title]
|
||||
other="Visualizador de bordados"
|
||||
[embroidery-viewer-description]
|
||||
other="Uma ferramenta online gratuita para visualizar arquivos de bordado escrita com Svelte."
|
||||
[lacine-title]
|
||||
other="Lacine"
|
||||
[lacine-description]
|
||||
other="Landing page desenvolvida com SvelteKit para divulgar o Lacine, um laboratório criativo para cineastas."
|
||||
[entrepreneurship-alagoas-title]
|
||||
other="Empreendedorismo Alagoas"
|
||||
[entrepreneurship-alagoas-description]
|
||||
other="Landing page desenvolvida com SvelteKit para divulgar o Lagoon Startup e o VAI Startup, dois programas que apoiam empreendedores em Alagoas com subvenção, mentoria e metodologia."
|
||||
[itr-title]
|
||||
other="Tecnologias Inovadoras para a Reparação (TIR)"
|
||||
[itr-description]
|
||||
other="Landing page desenvolvida com SvelteKit para divulgar um edital de projetos de desenvolvimento tecnológico, com capacitação e apoio financeiro."
|
||||
[iebtplus-title]
|
||||
other="IEBT+"
|
||||
[iebtplus-description]
|
||||
other="Plataforma desenvolvida com SvelteKit para conectar startups aos benefícios exclusivos do IEBT Plus. Também criei uma aplicação administrativa com Strapi para gerenciar os parceiros."
|
||||
[vumbora-startups-title]
|
||||
other="Vum Bora Startups"
|
||||
[vumbora-startups-description]
|
||||
other="Landing page e um CMS para um projeto que capacita, acelera e recompensa negócios com potencial de crescimento."
|
||||
[copyright]
|
||||
other="Leonardo Murça todos os direitos reservados."
|
||||
[rss-description-recent-content]
|
||||
other="Últimos"
|
||||
[rss-in]
|
||||
other=" "
|
||||
[rss-on]
|
||||
other="de"
|
||||
[all-rights-reserved]
|
||||
other="Todos os direitos reservados"
|
||||
[projectStatus_live]
|
||||
other = "No ar"
|
||||
[projectStatus_archived]
|
||||
other = "Arquivado"
|
||||
[my-resume]
|
||||
other = "Meu currículo"
|
||||
19
jsconfig.json
Normal file
19
jsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ .Site.LanguageCode }}">
|
||||
{{- partial "head.html" . -}}
|
||||
|
||||
<body>
|
||||
{{- partial "header.html" . -}}
|
||||
{{- block "main" . }}{{- end }}
|
||||
{{- partial "footer.html" . -}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{{ define "main"}}
|
||||
<main id="main">
|
||||
<section>
|
||||
<h1>{{i18n "posts" }}</h1>
|
||||
<div class="articles-list">
|
||||
<ul>
|
||||
{{ range .Pages }}
|
||||
<li>
|
||||
<time datetime="{{ .Date }}">{{ dateFormat (i18n "post-date-format") .Date }}</time>
|
||||
<a href="{{ .RelPermalink }}">{{ .Title }} </a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{{ end }}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
User-agent: *
|
||||
Sitemap: https://www.leomurca.xyz/sitemap.xml
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
{{- $pctx := . -}}
|
||||
{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
|
||||
{{- $pages := slice -}}
|
||||
{{- if or $.IsHome $.IsSection -}}
|
||||
{{- $pages = $pctx.RegularPages -}}
|
||||
{{- else -}}
|
||||
{{- $pages = $pctx.Pages -}}
|
||||
{{- end -}}
|
||||
{{- $limit := .Site.Config.Services.RSS.Limit -}}
|
||||
{{- if ge $limit 1 -}}
|
||||
{{- $pages = $pages | first $limit -}}
|
||||
{{- end -}}
|
||||
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} {{ i18n "rss-on"}} {{ end }}{{ .Site.Title }}{{ end }}</title>
|
||||
<link>{{ .Permalink }}</link>
|
||||
<description>{{ i18n "rss-description-recent-content" }} {{ if ne .Title .Site.Title }}{{ with .Title }} {{ i18n "rss-in"}} {{.}} {{ end }}{{ end }}{{ i18n "rss-on"}} {{ .Site.Title }}</description>
|
||||
{{ with .Site.LanguageCode }}
|
||||
<language>{{.}}</language>{{end}}{{ with .Site.Params.author.email }}
|
||||
{{end}}{{ with .Site.Params.author.email }}
|
||||
{{end}}{{ with (i18n .Site.Copyright) }}
|
||||
<copyright>{{ . }}</copyright>{{end}}{{ if not .Date.IsZero }}
|
||||
<lastBuildDate>{{ i18n .Date.Day }}{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
|
||||
{{- with .OutputFormats.Get "RSS" -}}
|
||||
{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
|
||||
{{- end -}}
|
||||
{{ range $pages }}
|
||||
<item>
|
||||
<title>{{ .Title }}</title>
|
||||
<link>{{ .Permalink }}</link>
|
||||
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
|
||||
{{ with .Site.Params.author.email }}<author>{{.}}{{ with $.Site.Params.author.name }} ({{.}}){{end}}</author>{{end}}
|
||||
<guid>{{ .Permalink }}</guid>
|
||||
<description>{{ .Content | html }}</description>
|
||||
</item>
|
||||
{{ end }}
|
||||
</channel>
|
||||
</rss>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{{ define "main" }}
|
||||
<main id="main">
|
||||
<article>
|
||||
<h1 class="title">{{ .Title }}</h1>
|
||||
<section class="body">
|
||||
{{ .Content }}
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
{{ end }}
|
||||
|
|
@ -1,257 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ .Site.LanguageCode }}">
|
||||
{{ partial "head.html" . }}
|
||||
|
||||
<body>
|
||||
{{ partial "header.html" . }}
|
||||
<main id="main">
|
||||
<section class="about-me-home">
|
||||
<div class="about-me-home-left">
|
||||
<span>{{ i18n "my-name-is"}}</span>
|
||||
<h1 class="about-me-home-name">Leonardo.</h1>
|
||||
<h2 class="about-me-home-current-position">
|
||||
{{ i18n "home-position"}}
|
||||
<a href="https://arctouch.com/" target="_blank">ArcTouch</a>
|
||||
</h2>
|
||||
<p>
|
||||
{{ i18n "home-introduction" (dict "years" (sub (now.Year) 2018)) }}
|
||||
</p>
|
||||
<div id="about-me-home-icons">
|
||||
<a
|
||||
class="my-resume"
|
||||
href="https://git.leomurca.xyz/leomurca/resume/releases/download/latest/leonardo-murca-resume.pdf"
|
||||
target="_blank"
|
||||
aria-label="{{ i18n "my-resume" }}"
|
||||
>
|
||||
<div>{{ i18n "my-resume" }}</div>
|
||||
</a>
|
||||
<div class="icons-divider"></div>
|
||||
<a
|
||||
href="https://git.leomurca.xyz/"
|
||||
target="_blank"
|
||||
aria-label="My personal git server."
|
||||
>
|
||||
<div class="main-icons">{{ partial "svg" "git" }}</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/in/leonardoamurca/"
|
||||
target="_blank"
|
||||
aria-label="My personal LinkedIn profile."
|
||||
>
|
||||
<div class="main-icons">{{ partial "svg" "linkedin" }}</div>
|
||||
</a>
|
||||
<a
|
||||
href="{{ .Site.Home.RelPermalink }}posts/index.xml"
|
||||
target="_blank"
|
||||
aria-label="My RSS Feed."
|
||||
>
|
||||
<div class="main-icons">{{ partial "svg" "rss" }}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
width="250"
|
||||
height="250"
|
||||
src="/img/avatar.webp"
|
||||
srcset="/img/avatar-mobile.webp 600w, /img/avatar.webp 1080w"
|
||||
sizes="50vw"
|
||||
class="avatar"
|
||||
alt="A picture of me smiling, wearing a blue t-shirt and a stone collar with some coconut trees behind me."
|
||||
/>
|
||||
</section>
|
||||
<section id="my-projects-section">
|
||||
<h2>{{ i18n "my-projects"}}</h2>
|
||||
<p>{{i18n "my-projects-description" }}</p>
|
||||
<div id="card-list">
|
||||
<article class="card">
|
||||
<img
|
||||
src="/img/embroidery-viewer/card-screenshot.webp"
|
||||
width="450"
|
||||
alt="Embroidery viewer screenshot."
|
||||
class="card__image"
|
||||
/>
|
||||
<div class="card__content">
|
||||
<header class="card__header">
|
||||
<h2 class="card__title">
|
||||
<a href="projects/embroidery-viewer">
|
||||
{{ i18n "embroidery-viewer-title" }}
|
||||
</a>
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<div class="card__tags">
|
||||
<div class="tag">
|
||||
<div
|
||||
class="tag__color"
|
||||
style="background-color: #f73b01"
|
||||
></div>
|
||||
<div>Svelte</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="card__description">
|
||||
{{ i18n "embroidery-viewer-description" }}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<img
|
||||
src="/img/lacine/card-screenshot.webp"
|
||||
width="450"
|
||||
alt="Embroidery viewer screenshot."
|
||||
class="card__image"
|
||||
/>
|
||||
<div class="card__content">
|
||||
<header class="card__header">
|
||||
<h2 class="card__title">
|
||||
<a href="projects/lacine">{{ i18n "lacine-title" }}</a>
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<div class="card__tags">
|
||||
<div class="tag">
|
||||
<div
|
||||
class="tag__color"
|
||||
style="background-color: #f73b01"
|
||||
></div>
|
||||
<div>SvelteKit</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card__description">{{ i18n "lacine-description" }}</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<img
|
||||
src="/img/entrepreneurship-alagoas/card-screenshot.webp"
|
||||
width="450"
|
||||
alt="Embroidery viewer screenshot."
|
||||
class="card__image"
|
||||
/>
|
||||
<div class="card__content">
|
||||
<header class="card__header">
|
||||
<h2 class="card__title">
|
||||
<a href="projects/entrepreneurship-alagoas">
|
||||
{{ i18n "entrepreneurship-alagoas-title" }}
|
||||
</a>
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<div class="card__tags">
|
||||
<div class="tag">
|
||||
<div
|
||||
class="tag__color"
|
||||
style="background-color: #f73b01"
|
||||
></div>
|
||||
<div>SvelteKit</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card__description">
|
||||
{{ i18n "entrepreneurship-alagoas-description" }}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<img
|
||||
src="/img/itr/card-screenshot.webp"
|
||||
width="450"
|
||||
alt="Embroidery viewer screenshot."
|
||||
class="card__image"
|
||||
/>
|
||||
<div class="card__content">
|
||||
<header class="card__header">
|
||||
|
||||
<h2 class="card__title">
|
||||
<a href="projects/itr">
|
||||
{{ i18n "itr-title" }}
|
||||
</a>
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<div class="card__tags">
|
||||
<div class="tag">
|
||||
<div
|
||||
class="tag__color"
|
||||
style="background-color: #f73b01"
|
||||
></div>
|
||||
<div>SvelteKit</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card__description">{{ i18n "itr-description" }}</p>
|
||||
</div>
|
||||
</article>
|
||||
{{/* <article class="card">
|
||||
<img
|
||||
src="/img/iebtplus/screenshot.png"
|
||||
width="450"
|
||||
alt="Embroidery viewer screenshot."
|
||||
class="card__image"
|
||||
/>
|
||||
<div class="card__content">
|
||||
<header class="card__header">
|
||||
<h2 class="card__title">{{ i18n "iebtplus-title" }}</h2>
|
||||
</header>
|
||||
|
||||
<div class="card__tags">
|
||||
<div class="tag">
|
||||
<div
|
||||
class="tag__color"
|
||||
style="background-color: #f73b01"
|
||||
></div>
|
||||
<div>SvelteKit</div>
|
||||
</div>
|
||||
|
||||
<div class="tag">
|
||||
<div
|
||||
class="tag__color"
|
||||
style="background-color: #4845fe"
|
||||
></div>
|
||||
<div>Strapi</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card__description">{{ i18n "iebtplus-description" }}</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<img
|
||||
src="/img/vumbora-startups/screenshot.png"
|
||||
width="450"
|
||||
alt="Embroidery viewer screenshot."
|
||||
class="card__image"
|
||||
/>
|
||||
<div class="card__content">
|
||||
<header class="card__header">
|
||||
<h2 class="card__title">{{ i18n "vumbora-startups-title" }}</h2>
|
||||
</header>
|
||||
|
||||
<div class="card__tags">
|
||||
<div class="tag">
|
||||
<div
|
||||
class="tag__color"
|
||||
style="background-color: #f73b01"
|
||||
></div>
|
||||
<div>SvelteKit</div>
|
||||
</div>
|
||||
|
||||
<div class="tag">
|
||||
<div
|
||||
class="tag__color"
|
||||
style="background-color: #4845fe"
|
||||
></div>
|
||||
<div>Strapi</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card__description">
|
||||
{{ i18n "vumbora-startups-description" }}
|
||||
</p>
|
||||
</div>
|
||||
</article> */}}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{{ partial "footer.html" . }}
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<footer>
|
||||
<div id="footer-icons">
|
||||
<a href="https://git.leomurca.xyz/" target="_blank" aria-label="My personal git server.">
|
||||
<div>{{ partial "svg" "git" }}</div>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/in/leonardoamurca/" target="_blank" aria-label="My personal LinkedIn profile.">
|
||||
<div>{{ partial "svg" "linkedin" }}</div>
|
||||
</a>
|
||||
<a href="{{ .Site.Home.RelPermalink }}posts/index.xml" target="_blank" aria-label="My RSS feed.">
|
||||
<div>{{ partial "svg" "rss" }}</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>Copyright © {{ now.Year }} Leonardo Murça. <br /> {{ i18n "all-rights-reserved"}} </p>
|
||||
</footer>
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
<head>
|
||||
{{- $title := ( .Title ) -}}
|
||||
{{- $siteTitle := ( .Site.Title ) -}}
|
||||
{{- if .IsHome -}}
|
||||
<title>{{ $siteTitle }} | {{ i18n "home" }}</title>
|
||||
{{- else -}}
|
||||
<title>{{ $title }} - {{ $siteTitle }}</title>
|
||||
{{- end -}}
|
||||
{{ range .AlternativeOutputFormats -}}
|
||||
{{ printf `
|
||||
<link rel="%s" type="%s" href="%s" title="%s" />` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }}
|
||||
{{ end -}}
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description"
|
||||
content="{{ if $.IsHome }}{{ i18n $.Site.Params.description }}{{else}}{{$.Description}}{{end}}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="author" content="Leonardo Murça" />
|
||||
<meta property="og:locale" content="{{ .Site.LanguageCode }}">
|
||||
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
|
||||
<meta property="og:title"
|
||||
content="{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} · {{ .Site.Title }}{{ end }}">
|
||||
<meta property="og:description"
|
||||
content="{{ if $.IsHome }}{{ i18n $.Site.Params.description }}{{else}}{{$.Description}}{{end}}" />
|
||||
<meta property="og:url" content="{{ .Permalink }}">
|
||||
<meta property="og:site_name" content="{{ .Site.Title }}">
|
||||
{{ if .Params.featured_image }}
|
||||
<meta property="og:image" content="{{ .Params.featured_image }}">
|
||||
<meta property="og:image:secure_url" content="{{ .Params.featured_image }}">
|
||||
{{- end }}
|
||||
{{ if .IsHome }}
|
||||
<meta property="og:image" content="{{ .Site.Params.featured_image }}">
|
||||
<meta property="og:image:secure_url" content="{{ .Site.Params.featured_image }}">
|
||||
{{- end }}
|
||||
{{ if isset .Params "date" }}
|
||||
<meta property="article:published_time" content="{{ (time .Date).Format " 2006-01-02T15:04:05Z" }}">{{ end }}
|
||||
<meta name="apple-mobile-web-app-title" content="Leonardo Murça" />
|
||||
<link rel="icon" type="image/png" href="/img/favicon/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/img/favicon/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/img/favicon/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/styles.css" />
|
||||
<link rel="preload" as="font">
|
||||
<script async src="https://hk.leomurca.xyz/hk.js"></script>
|
||||
</head>
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<header>
|
||||
<a href="#main" class="skip-to-main-content-link">Skip to main content</a>
|
||||
<a class="main-title" href="{{ .Site.Home.RelPermalink }}">
|
||||
<img src="/img/logo.svg" width="150" height="67" alt="Logotype" />
|
||||
</a>
|
||||
|
||||
<input aria-hidden="true" tabindex="-1" class="menu-btn" type="checkbox" id="menu-btn" />
|
||||
<label tabindex="-1" class="menu-icon" for="menu-btn">
|
||||
<span tabindex="-1" class="navicon"></span>
|
||||
</label>
|
||||
|
||||
|
||||
<nav class="menu">
|
||||
{{ range .Site.Menus.main }}
|
||||
<a class='nav-item' href="{{ .URL }}">{{ .Name }}</a>
|
||||
{{ end }}
|
||||
</nav>
|
||||
|
||||
{{/* {{ if eq "pt-br" .Site.LanguageCode}}
|
||||
<a class="common-switch english-switch" href="{{ .Site.BaseURL }}">
|
||||
<div style="display: flex; width: fit-content;">
|
||||
<div class="language-icon">{{ partial "svg" "language" }}</div>
|
||||
<span>English</span>
|
||||
</div>
|
||||
</a>
|
||||
{{ else }}
|
||||
<a class="common-switch portuguese-switch" href="{{ .Site.BaseURL }}/pt-br">
|
||||
<div style="display: flex; width: fit-content;">
|
||||
<div class="language-icon">{{ partial "svg" "language" }}</div>
|
||||
<span>Português</span>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }} */}}
|
||||
|
||||
{{ $lang := .Site.Language.Lang }}
|
||||
{{ $isPT := eq $lang "pt-br" }}
|
||||
|
||||
{{ $currentPath := .RelPermalink }}
|
||||
{{ $targetPath := "" }}
|
||||
|
||||
{{ if $isPT }}
|
||||
{{/* Remove /pt-br prefix from path */}}
|
||||
{{ $targetPath = replace $currentPath "/pt-br" "" }}
|
||||
{{ else }}
|
||||
{{/* Add /pt-br prefix */}}
|
||||
{{ $targetPath = print "/pt-br" $currentPath }}
|
||||
{{ end }}
|
||||
|
||||
<a class="common-switch {{ if $isPT }}english-switch{{ else }}portuguese-switch{{ end }}"
|
||||
href="{{ $targetPath }}">
|
||||
<div style="display: flex; width: fit-content;">
|
||||
<div class="language-icon">{{ partial "svg" "language" }}</div>
|
||||
<span>{{ if $isPT }}English{{ else }}Português{{ end }}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
|
||||
</header>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{{- $statusRaw := .Params.status | default "unknown" -}}
|
||||
{{- $status := lower $statusRaw -}}
|
||||
{{- $class := replace $status " " "-" -}}
|
||||
|
||||
<span class="project-status-label">Status</span>
|
||||
<span class="project-status project-status--{{ $class }}">
|
||||
{{ i18n (print "projectStatus_" $status) | default (title $statusRaw) }}
|
||||
</span>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{{ $svg := . }}
|
||||
{{ $class := print $svg "-icon" }}
|
||||
{{ $match := "<svg (.*)?>(.*)</svg>" }}
|
||||
|
||||
|
||||
{{ $replaceWith := printf `<svg class="%s" ${1}>${2}</svg>` $class }}
|
||||
{{ return (replaceRE $match $replaceWith (printf "/static/svg/%s.svg" $svg | readFile) | safeHTML) }}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{{ $imgPath := delimit (slice "static" (.Destination | safeURL) ) "/"}}
|
||||
|
||||
{{ if fileExists $imgPath }}
|
||||
|
||||
{{ $img := imageConfig $imgPath }}
|
||||
|
||||
<picture>
|
||||
<img src="{{ .Destination | safeURL }}" width="{{ $img.Width }}" height="{{ $img.Height }}" alt="{{ .Text }}" {{ with
|
||||
.Title}} title="{{ . }}" {{ end }} />
|
||||
<figcaption>Fig. {{ substr (strings.TrimSuffix (path.Ext .Destination) .Destination) -1 }} - {{ .Text }}.</figcaption>
|
||||
</picture>
|
||||
|
||||
{{ else }}
|
||||
|
||||
{{ errorf "Specified file at %s not found." $imgPath }}
|
||||
|
||||
{{ end }}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{{ define "main" }}
|
||||
<main id="main">
|
||||
<article>
|
||||
<h1 class="title">{{ .Title }}</h1>
|
||||
<p class="posted-on"><strong>{{ i18n "posted-on"}}</strong>{{ dateFormat (i18n "single-post-date-format") .Date }}</p>
|
||||
{{ if ne .Date .Lastmod }}
|
||||
<p class="last-update"><strong>{{ i18n "last-update"}}</strong>{{ dateFormat (i18n "single-post-date-format") .Lastmod }}</p>
|
||||
{{ end }}
|
||||
<section class="body">
|
||||
{{ .Content }}
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
{{ end }}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
{{ define "main" }}
|
||||
<main id="main">
|
||||
<article class="project-container">
|
||||
<div class="video-container">
|
||||
<video
|
||||
src="{{ .Params.lead_video }}"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
style="width: 100%; height: auto"
|
||||
></video>
|
||||
<div class="video-overlay">
|
||||
<h1 class="project-title">{{ .Title }}</h1>
|
||||
<p>
|
||||
{{ .Description }}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-meta">
|
||||
{{ partial "project-status.html" . }}
|
||||
</div>
|
||||
<section class="body">{{ .Content }}</section>
|
||||
</article>
|
||||
</main>
|
||||
{{ end }}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<img class="icon" src="/icons/{{ .Get "src" }}"
|
||||
{{- with .Get "alt" }} alt="{{.}}"{{ end -}}
|
||||
>
|
||||
3292
package-lock.json
generated
Normal file
3292
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
38
package.json
Normal file
38
package.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "leomurca",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"accept-language-parser": "^1.5.0",
|
||||
"sveltekit-i18n": "^2.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.0.4",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@sveltejs/adapter-node": "^5.5.4",
|
||||
"@sveltejs/kit": "^2.57.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||
"@types/node": "^22",
|
||||
"eslint": "^10.2.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.17.0",
|
||||
"globals": "^17.4.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"svelte": "^5.55.2",
|
||||
"svelte-check": "^4.4.6",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^8.0.7"
|
||||
}
|
||||
}
|
||||
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
12
src/app.html
Normal file
12
src/app.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="text-scale" content="scale" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
44
src/lib/assets/arctouch-logo-white.svg
Normal file
44
src/lib/assets/arctouch-logo-white.svg
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
width="1995.1549"
|
||||
height="337.34668"
|
||||
viewBox="0 0 1995.1549 337.34668"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1">
|
||||
<color-profile
|
||||
name="sRGB IEC61966-2.1"
|
||||
xlink:href="data:application/vnd.iccprofile;base64,AAAMbExpbm8CEAAAbW50clJHQiBYWVogB84AAgAJAAYAMQAAYWNzcE1TRlQAAAAASUVDIHNSR0IAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1IUCAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARY3BydAAAAVAAAAAzZGVzYwAAAYQAAACQd3RwdAAAAhQAAAAUYmtwdAAAAigAAAAUclhZWgAAAjwAAAAUZ1hZWgAAAlAAAAAUYlhZWgAAAmQAAAAUZG1uZAAAAngAAABwZG1kZAAAAugAAACIdnVlZAAAA3AAAACGdmlldwAAA/gAAAAkbHVtaQAABBwAAAAUbWVhcwAABDAAAAAkdGVjaAAABFQAAAAMclRSQwAABGAAAAgMZ1RSQwAABGAAAAgMYlRSQwAABGAAAAgMdGV4dAAAAABDb3B5cmlnaHQgKGMpIDE5OTggSGV3bGV0dC1QYWNrYXJkIENvbXBhbnkAAGRlc2MAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAASAHMAUgBHAEIAIABJAEUAQwA2ADEAOQA2ADYALQAyAC4AMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAADzUQABAAAAARbMWFlaIAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9kZXNjAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4gSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2aWV3AAAAAAATpP4AFF8uABDPFAAD7cwABBMLAANcngAAAAFYWVogAAAAAABMCVYAUAAAAFcf521lYXMAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAKPAAAAAnNpZyAAAAAAQ1JUIGN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4AkECSwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oDZgNyA34DigOWA6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7BowGnQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiqCL4I0gjnCPsJEAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApqCoEKmAquCsUK3ArzCwsLIgs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUMjgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4uDkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPURExExEU8RbRGMEaoRyRHoEgcSJhJFEmQShBKjEsMS4xMDEyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVWFXgVmxW9FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhAGGUYihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzMHPUdHh1HHXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUhoSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYnJlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDksbiyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/iQCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fASAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxOJU5uTrdPAE9JT5NP3VAnUHFQu1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX4FgvWH1Yy1kaWWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+zYAVgV2CqYPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iWaOxpQ2maafFqSGqfavdrT2una/9sV2yvbQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMBc11zuHQUdHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4BfmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZif6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSVX5XJljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobaiJqKWowajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqPqwKrdavprFys0K1ErbiuLa6hrxavi7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsuu6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HHv8g9yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7ijutO9A78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY/Sn9uv5L/tz/bf//"
|
||||
id="color-profile1" />
|
||||
</defs>
|
||||
<g
|
||||
id="layer-MC0"
|
||||
transform="translate(-164.40761,-164.41161)">
|
||||
<path
|
||||
id="path2"
|
||||
d="m 0,0 h 68.854 v -55.559 h 9.561 V 53.712 h -9.803 v -44.8 h -68.77 v 44.8 H -9.961 V -55.559 H 0 Z m -158.435,-1.285 c 0,33.239 23.219,57.406 54.793,57.406 18.964,0 35.192,-8.751 45.235,-23.926 l -7.956,-5.861 c -7.552,13.168 -21.609,20.714 -37.279,20.714 -25.627,0 -44.749,-20.232 -44.749,-48.252 0,-28.021 19.122,-48.012 44.749,-48.012 17.113,0 30.933,8.51 40.091,24.728 l 7.714,-5.86 c -9.964,-18.065 -27.074,-27.941 -47.884,-27.941 -31.574,0 -54.793,23.926 -54.793,57.165 z M -300.8,53.632 v -74.988 c 0,-26.335 11.57,-37.093 43.385,-37.093 31.815,0 43.143,10.758 43.143,37.093 v 74.988 h -9.802 v -71.375 c 0,-24.97 -7.552,-31.634 -33.583,-31.634 -26.03,0 -33.582,6.664 -33.582,31.634 V 53.632 Z M -449.512,-1.285 c 0,-28.02 19.36,-48.011 44.749,-48.011 25.39,0 44.991,20.312 44.991,48.011 0,27.7 -19.123,48.253 -44.75,48.253 -25.63,0 -44.99,-20.232 -44.99,-48.253 m -9.964,0 c 0,33.239 23.218,57.406 54.792,57.406 31.574,0 55.196,-24.086 55.196,-57.406 0,-33.319 -23.381,-57.164 -54.955,-57.164 -31.573,0 -54.954,23.925 -54.954,57.164 z m -87.332,-54.274 h 9.561 v 100.36 h 46.197 v 8.911 h -101.954 v -8.911 h 46.196 z M -873.239,6.824 v 30.831 h 46.999 c 12.214,0 18.32,-5.861 18.32,-15.576 0,-9.715 -5.864,-15.174 -18.881,-15.174 h -46.438 z m 0,-16.137 h 39.289 l 27.477,-46.246 h 19.763 l -27.877,47.048 c 15.504,3.694 25.065,16.058 25.065,30.992 0,19.67 -14.061,31.231 -36.477,31.231 h -64.915 V -55.559 h 17.675 z m -103.883,-1.686 h -41.697 l 20.728,40.384 z m -50.132,-16.218 h 58.249 l 14.864,-28.342 h 19.36 l -56.398,109.271 h -13.82 l -55.758,-109.271 h 19.122 l 14.461,28.342 z m 281.521,25.932 c 0,33.239 24.824,57.406 58.81,57.406 20.969,0 39.043,-9.313 50.854,-27.137 l -15.022,-10.116 c -7.552,13.97 -20.569,21.115 -35.832,21.115 -23.219,0 -40.736,-17.021 -40.736,-41.187 0,-24.167 17.517,-40.947 40.736,-40.947 16.307,0 29.324,7.708 38.885,25.13 l 14.219,-10.357 c -11.408,-20.473 -30.127,-30.991 -53.104,-30.991 -33.986,0 -58.81,23.926 -58.81,57.165 z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,2055.0091,341.21467)" />
|
||||
<path
|
||||
id="path3"
|
||||
d="m 0,0 h -29.235 c -33.992,0 -61.65,27.658 -61.65,61.656 0,33.995 27.658,61.653 61.65,61.653 33.998,0 61.656,-27.658 61.656,-61.653 V 0 H 12.802 v 61.656 c 0,23.178 -18.859,42.034 -42.037,42.034 -23.178,0 -42.031,-18.856 -42.031,-42.034 0,-23.178 18.853,-42.037 42.031,-42.037 H 0 Z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,372.0612,415.29333)" />
|
||||
<path
|
||||
id="path4"
|
||||
d="m 0,0 h -61.663 c -51.872,0 -94.077,42.205 -94.077,94.077 0,51.873 42.205,94.078 94.077,94.078 51.879,0 94.084,-42.205 94.084,-94.078 V 0 H 12.802 v 94.077 c 0,41.056 -33.406,74.459 -74.465,74.459 -41.059,0 -74.458,-33.403 -74.458,-74.459 0,-41.059 33.399,-74.458 74.458,-74.458 H 0 Z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,415.29787,458.52147)" />
|
||||
<path
|
||||
id="path5"
|
||||
d="m 0,0 h -19.619 v 126.505 c 0,58.937 -47.946,106.886 -106.886,106.886 -58.94,0 -106.886,-47.949 -106.886,-106.886 0,-58.94 47.946,-106.886 106.886,-106.886 h 94.084 V 0 h -94.084 c -69.753,0 -126.505,56.751 -126.505,126.505 0,69.753 56.752,126.505 126.505,126.505 C -56.751,253.01 0,196.258 0,126.505 Z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,501.75427,501.75827)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
41
src/lib/assets/ciandt-logo-white.svg
Normal file
41
src/lib/assets/ciandt-logo-white.svg
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
width="1551.3"
|
||||
height="499.89996"
|
||||
viewBox="0 0 1551.3 499.89997"
|
||||
xml:space="preserve"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1"><color-profile
|
||||
name="sRGB IEC61966-2.1"
|
||||
xlink:href="data:application/vnd.iccprofile;base64,AAAMbExpbm8CEAAAbW50clJHQiBYWVogB84AAgAJAAYAMQAAYWNzcE1TRlQAAAAASUVDIHNSR0IAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1IUCAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARY3BydAAAAVAAAAAzZGVzYwAAAYQAAACQd3RwdAAAAhQAAAAUYmtwdAAAAigAAAAUclhZWgAAAjwAAAAUZ1hZWgAAAlAAAAAUYlhZWgAAAmQAAAAUZG1uZAAAAngAAABwZG1kZAAAAugAAACIdnVlZAAAA3AAAACGdmlldwAAA/gAAAAkbHVtaQAABBwAAAAUbWVhcwAABDAAAAAkdGVjaAAABFQAAAAMclRSQwAABGAAAAgMZ1RSQwAABGAAAAgMYlRSQwAABGAAAAgMdGV4dAAAAABDb3B5cmlnaHQgKGMpIDE5OTggSGV3bGV0dC1QYWNrYXJkIENvbXBhbnkAAGRlc2MAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAASAHMAUgBHAEIAIABJAEUAQwA2ADEAOQA2ADYALQAyAC4AMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAADzUQABAAAAARbMWFlaIAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9kZXNjAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4gSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2aWV3AAAAAAATpP4AFF8uABDPFAAD7cwABBMLAANcngAAAAFYWVogAAAAAABMCVYAUAAAAFcf521lYXMAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAKPAAAAAnNpZyAAAAAAQ1JUIGN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4AkECSwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oDZgNyA34DigOWA6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7BowGnQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiqCL4I0gjnCPsJEAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApqCoEKmAquCsUK3ArzCwsLIgs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUMjgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4uDkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPURExExEU8RbRGMEaoRyRHoEgcSJhJFEmQShBKjEsMS4xMDEyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVWFXgVmxW9FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhAGGUYihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzMHPUdHh1HHXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUhoSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYnJlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDksbiyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/iQCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fASAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxOJU5uTrdPAE9JT5NP3VAnUHFQu1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX4FgvWH1Yy1kaWWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+zYAVgV2CqYPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iWaOxpQ2maafFqSGqfavdrT2una/9sV2yvbQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMBc11zuHQUdHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4BfmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZif6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSVX5XJljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobaiJqKWowajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqPqwKrdavprFys0K1ErbiuLa6hrxavi7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsuu6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HHv8g9yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7ijutO9A78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY/Sn9uv5L/tz/bf//"
|
||||
id="color-profile1" /></defs><g
|
||||
id="layer-MC0"
|
||||
transform="translate(1868.65,1403.9499)"><g
|
||||
id="Layer_1-2"
|
||||
transform="translate(-1868.65,-1403.95)"
|
||||
style="fill:#ffffff;fill-opacity:1"><path
|
||||
class="s0"
|
||||
d="M 0,251.2 C 0,95.1 111,1.3 261.4,1.3 383.1,1.3 479,61.5 506.9,180.8 H 390.1 C 376.8,107.9 333.7,55.2 263.3,55.2 c -85.7,0 -137.1,80.6 -137.1,180.8 0,114.2 57.1,185.9 154.8,185.9 88.2,0 145.3,-57.1 161.1,-145.3 H 510 C 491,413.7 395.2,499.9 253.1,499.9 100.2,499.9 0,398.4 0,251.2 Z"
|
||||
id="path1-4"
|
||||
style="fill:#ffffff;fill-opacity:1" /><path
|
||||
class="s0"
|
||||
d="m 535.5,487.2 c 22.9,-17.7 28,-46.3 28,-96.4 V 108.5 c 0,-51.4 -5.1,-78.7 -28,-96.5 V 9.5 H 711.2 V 12 c -22.8,17.8 -27.9,45.1 -27.9,96.5 v 282.3 c 0,50.1 5.1,78.7 27.9,96.4 v 2.6 H 535.5 Z"
|
||||
id="path2-6"
|
||||
style="fill:#ffffff;fill-opacity:1" /><path
|
||||
class="s0"
|
||||
d="m 1128.2,9.5 h 423.1 V 98.3 C 1481.6,71 1445.4,58.4 1402.9,58.4 h -3.2 v 332.4 c 0,50.1 5.1,78.7 28,96.4 v 2.6 h -175.1 v -2.6 c 22.8,-17.7 27.9,-46.3 27.9,-96.4 V 58.4 h -2.6 c -43.1,0 -79.2,12.6 -149.7,39.9 z"
|
||||
id="path3-4"
|
||||
style="fill:#ffffff;fill-opacity:1" /><path
|
||||
class="s0"
|
||||
d="m 736.7,361.3 c 0,-67.4 44.6,-114.5 113.9,-141.2 -30.5,-35 -48.4,-66.2 -48.4,-104.3 C 802.2,43.3 862.7,0 941.5,0 c 86.5,0 141.2,55.3 148.2,139.9 h -89 c -0.6,-53.4 -19.1,-96.6 -60.4,-96.6 -29.9,0 -48.4,19.7 -48.4,50.9 0,31.1 18.5,59.1 71.2,112.5 L 1070,314.3 c 6.4,-28 10.2,-58.6 10.8,-89.1 l 95.4,-0.6 v 15.2 c -21,41.4 -42,81.4 -64.8,117 l 122.7,123.4 v 9.6 H 1113.2 L 1054.7,430 c -41.3,42 -91.5,68.7 -157.7,68.7 -94.1,0 -160.3,-53.5 -160.3,-137.4 z m 205.5,73.1 c 36.2,0 63.6,-12.7 84.6,-33.7 l -149.5,-152 c -28,19.7 -41.4,49.6 -41.4,82.7 0,59.8 41.4,103 106.3,103 z"
|
||||
id="path4-6"
|
||||
style="fill:#ffffff;fill-opacity:1" /></g></g><style
|
||||
id="style1">
|
||||
.s0 { fill: #fa5a50 }
|
||||
</style></svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
27
src/lib/assets/disney-logo-white.svg
Normal file
27
src/lib/assets/disney-logo-white.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.7 MiB |
21
src/lib/assets/favicon.svg
Normal file
21
src/lib/assets/favicon.svg
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="194.20682mm"
|
||||
height="156.94582mm"
|
||||
viewBox="0 0 194.20682 156.94582"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
id="layer1"
|
||||
transform="translate(339.70607,-339.04412)"><path
|
||||
style="display:inline;fill:#fc871f;fill-opacity:1"
|
||||
d="m -278.45771,463.96495 c -1.28064,-2.48646 -3.23885,-5.54137 -4.33905,-8.89723 -0.56397,-1.93275 -0.62404,-4.70681 -0.69142,-31.92639 -0.0406,-16.39535 0.0641,-31.12031 0.23261,-32.72215 0.36067,-3.4284 1.17076,-5.44162 3.00784,-7.47505 2.00752,-2.22208 3.72807,-2.99588 6.6784,-3.00352 2.14373,-0.006 2.81842,0.16591 5.11528,1.30002 3.10182,1.53157 4.20074,2.4493 30.31919,25.32014 10.52502,9.21632 19.7739,17.14059 20.55306,17.60949 1.05397,0.63429 1.92602,0.85208 3.40623,0.85073 3.02423,-0.003 2.99988,0.0144 13.45059,-9.46438 2.56708,-2.32834 5.57881,-5.02709 6.69274,-5.99723 1.11392,-0.97013 6.27724,-5.57388 11.47404,-10.23055 16.89088,-15.13532 19.52862,-17.39767 21.9878,-18.85864 5.26743,-3.12932 9.4068,-2.57736 12.66024,1.68819 2.4859,3.25924 2.35251,0.41054 2.40634,51.38989 0.0266,25.22361 -0.0666,47.38153 -0.20716,49.23981 -0.19301,2.55154 -0.47299,3.80772 -1.14364,5.13118 -1.4785,2.91769 -3.85973,5.25546 -6.86747,6.74216 -2.56901,1.26983 -2.84441,1.32852 -6.23416,1.32852 -3.11452,0 -3.79356,-0.11582 -5.57581,-0.95105 -2.83901,-1.33045 -5.68121,-4.13009 -7.0663,-6.96047 l -1.13486,-2.31904 -0.17638,-24.40437 c -0.17409,-24.08605 -0.18604,-24.41737 -0.91575,-25.4 -1.06111,-1.42888 -2.89266,-1.8199 -4.5005,-0.9608 -0.69043,0.3689 -7.25516,6.23981 -14.58829,13.04647 -17.7355,16.46219 -19.0948,17.67837 -20.50871,18.34931 -1.69478,0.80423 -4.61682,0.52492 -6.18502,-0.5912 -1.17058,-0.83313 -12.04421,-10.15478 -15.57956,-13.35592 -0.97703,-0.88467 -3.33991,-2.95786 -5.25084,-4.6071 -3.31016,-2.85684 -9.71138,-8.46422 -11.40537,-9.99095 l -0.79375,-0.71537 0.14718,28.63126 c 0,0 72.47879,53.24691 -24.9675,-1.79576 z"
|
||||
id="path1" /><path
|
||||
style="fill:#ef3826;fill-opacity:1"
|
||||
d="m -283.32309,495.62791 c -23.07343,-0.24221 -23.31134,-0.25797 -29.11453,-1.92799 -3.01241,-0.86691 -8.80358,-3.70689 -11.27852,-5.53097 -7.53433,-5.55295 -12.56531,-12.73221 -14.904,-21.26813 l -0.96655,-3.52778 -0.10018,-53.94876 c -0.0624,-33.61888 0.0292,-54.8822 0.24309,-56.4258 0.64175,-4.63096 4.0281,-9.75053 7.96653,-12.04402 7.89424,-4.5971 17.90951,-0.58134 21.59624,8.65931 0.58949,1.47753 0.63977,4.63823 0.80514,50.61205 l 0.17639,49.03611 0.82472,2.46945 c 2.01576,6.03573 5.22147,9.3852 10.86411,11.35132 2.68451,0.93539 3.05708,0.97074 11.41811,1.08342 5.68115,0.0766 8.46749,-10e-4 8.13074,-0.22683 -0.28178,-0.18881 20.77607,-0.005 23.97206,0.56189 l 3.43959,0.22437 c 6.74878,0.44023 11.73086,2.51223 16.23992,6.75403 3.13175,2.94613 5.6233,6.40219 9.9072,13.74239 2.4559,4.20805 2.86884,5.14797 2.86884,6.53 0,1.7713 -1.0489,3.29309 -2.70335,3.92211 -0.93451,0.3553 -22.75786,0.33834 -59.38553,-0.0462 z"
|
||||
id="path1-4" /></g></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
75
src/lib/assets/iebt-logo-white.svg
Normal file
75
src/lib/assets/iebt-logo-white.svg
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="239.86714mm"
|
||||
height="122.91834mm"
|
||||
viewBox="0 0 239.86714 122.91834"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
id="layer1"
|
||||
transform="translate(113.00196,812.27897)"><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.441941;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m -112.2772,-726.80835 c 0.39862,-2.41167 0.73056,-15.56636 0.73764,-29.23259 l 0.0129,-24.84771 h 7.01583 7.015823 v 29.2326 29.23259 h -7.753473 -7.75347 z"
|
||||
id="path318" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m -112.7063,-722.06617 c 0,-0.19423 -0.0513,-0.37023 -0.11403,-0.39113 -0.081,-0.027 -0.066,-0.34015 0.0517,-1.08032 0.14347,-0.90244 0.1657,-2.34545 0.1657,-10.7538 0,-5.34133 0.031,-18.23035 0.0688,-28.64226 l 0.0688,-18.93073 h 7.68266 7.682654 v 30.07567 30.07569 h -7.803144 -7.80314 z"
|
||||
id="path319" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m -112.59065,-790.36507 c 0.0352,-2.06066 0.0701,-5.68841 0.0775,-8.06164 l 0.0136,-4.31499 h 7.54476 7.544757 v 8.06154 8.06151 l -7.622277,1.3e-4 -7.62227,1.1e-4 z"
|
||||
id="path320" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0781251;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m -45.83322,-720.59942 c -6.207651,-0.59875 -12.790747,-3.00562 -17.108675,-6.25517 -0.966348,-0.72724 -2.597359,-2.19335 -3.624466,-3.258 -5.034206,-5.21825 -7.857014,-11.21873 -9.006046,-19.14433 -0.586121,-4.04286 -0.484638,-10.74285 0.223073,-14.72745 1.214215,-6.83635 3.70609,-12.54067 7.411989,-16.96733 3.257407,-3.89091 6.133534,-6.26512 9.64633,-7.96293 4.973328,-2.40371 8.787638,-3.25262 14.572753,-3.24339 7.652374,0.0122 12.968737,1.30127 18.025763,4.37068 8.294483,5.03438 13.332363,13.35844 15.156629,25.04321 0.206462,1.32241 0.38303,4.12887 0.451921,7.18301 l 0.1130683,5.01261 H -35.328873 -60.686865 l 0.140034,0.77515 c 0.843277,4.66791 3.032461,8.44894 6.512073,11.24728 3.398118,2.73278 7.641489,3.86151 13.623787,3.62387 5.239557,-0.20815 8.849881,-1.50186 11.791503,-4.22535 1.497044,-1.38604 2.975433,-3.28237 3.626212,-4.65132 l 0.368491,-0.77515 4.147505,0.0243 c 2.28113,0.0135 5.403245,0.0832 6.938035,0.15502 l 2.790527,0.13063 -0.06708,0.72345 c -0.192848,2.08007 -2.146589,6.52108 -4.31948,9.81853 -2.390899,3.62831 -4.562322,5.77058 -8.325846,8.21409 -3.377246,2.19274 -6.251261,3.34703 -10.417647,4.18407 -4.204044,0.84463 -8.112826,1.07495 -11.954473,0.7044 z m 21.131884,-42.3386 c 0,-0.10663 -0.136345,-0.781 -0.302987,-1.4986 -1.285478,-5.53559 -4.531588,-9.83604 -9.102122,-12.05857 -2.534457,-1.23246 -4.508508,-1.74921 -7.2347,-1.89391 -7.025058,-0.37283 -12.531707,1.74585 -15.788915,6.07483 -1.851189,2.46033 -3.538072,6.5019 -3.538072,8.47683 0,0.80848 0.155776,0.86137 2.790529,0.9475 5.09996,0.16668 33.176267,0.12602 33.176267,-0.0481 z"
|
||||
id="path321" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.00244141;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m -76.047689,-752.48798 c -0.0075,-0.0859 -0.0306,-0.46191 -0.04567,-0.74446 -0.135644,-2.54291 -0.06445,-5.1644 0.209907,-7.72885 0.02991,-0.27959 0.08232,-0.73282 0.0872,-0.75417 l 0.0033,-0.0145 h 2.803314 2.803313 l 0.0016,4.64441 0.0016,4.64442 h -2.930263 -2.930266 z"
|
||||
id="path322" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0781251;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m 40.100911,-720.50396 c -1.178354,-0.19 -3.934217,-0.9194 -5.84337,-1.54657 -2.384918,-0.78346 -3.708033,-1.65965 -6.496235,-4.30186 -1.20638,-1.14324 -2.248866,-2.07859 -2.316636,-2.07859 -0.06777,0 -0.123218,1.48828 -0.123218,3.30729 v 3.30729 H 17.67334 10.025228 v -45.139 -45.139 l 6.356201,-0.13568 c 3.49591,-0.0746 6.821289,-0.0623 7.38973,0.0273 l 1.033528,0.16293 0.132595,1.23847 c 0.07293,0.68114 0.14269,6.98227 0.155029,14.00254 0.01234,7.02024 0.08186,12.76405 0.154495,12.76405 0.07263,0 0.843384,-0.67958 1.71278,-1.51018 3.689739,-3.52518 6.720689,-5.09662 11.632311,-6.03102 2.296734,-0.43696 8.293335,-0.43397 11.253502,0.006 4.151294,0.6164 8.897709,2.55638 12.929975,5.28474 2.666077,1.80395 6.707172,5.87978 8.484997,8.55792 6.644344,10.00911 8.099978,23.34704 3.859231,35.36198 -2.246884,6.36593 -6.146035,11.76822 -11.255277,15.59431 -2.119778,1.58742 -6.215118,3.68609 -9.010975,4.61774 -3.800451,1.26641 -5.80462,1.59579 -10.204913,1.6772 -2.160074,0.0399 -4.206462,0.0276 -4.547526,-0.0274 z m 6.717937,-14.42961 c 5.934925,-0.90358 10.4306,-4.18727 13.029846,-9.51711 0.554431,-1.13689 1.221417,-2.85771 1.48219,-3.82405 0.445318,-1.65023 0.473517,-2.12754 0.464058,-7.85482 -0.01116,-6.7518 -0.118806,-7.4985 -1.556268,-10.795 -3.239434,-7.42894 -8.325072,-11.04365 -16.124423,-11.46082 -5.975456,-0.31959 -10.464837,1.08392 -13.630476,4.26132 -2.356186,2.36493 -4.114781,6.08579 -4.8412,10.24303 -0.678556,3.88334 -0.579076,12.13011 0.190453,15.78832 0.61718,2.93399 2.129857,6.13045 3.767372,7.96094 1.86649,2.08645 4.887347,4.00027 7.503279,4.75356 2.566379,0.73903 6.576134,0.92255 9.715169,0.44463 z"
|
||||
id="path323" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.110485;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m 109.35284,-722.16951 c -4.96023,-0.82331 -9.400492,-5.17097 -10.586542,-10.36577 -0.21867,-0.95776 -0.372586,-8.55691 -0.462769,-22.84793 l -0.135126,-21.41289 h -3.629654 -3.629652 v -6.43117 -6.43118 h 3.654075 3.654076 v -6.43117 -6.43118 h 7.600472 7.60048 v 6.43118 6.43117 h 6.13885 6.13885 v 6.43118 6.43117 h -6.02647 -6.02646 l 0.13019,19.65894 c 0.13556,20.46959 0.16511,20.93317 1.36551,21.42982 0.25959,0.10739 3.00425,0.26442 6.09925,0.3489 l 5.62728,0.15365 v 6.683 6.68298 l -7.96589,-0.0342 c -4.38123,-0.0188 -8.67715,-0.15224 -9.54647,-0.29654 z"
|
||||
id="path324" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m -77.577692,-697.48987 v -7.6998 h 2.222084 2.222088 v 7.6998 7.6998 h -2.222088 -2.222084 z"
|
||||
id="path325" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m -77.577692,-708.74218 v -1.71846 h 2.368338 2.368338 l -0.05678,1.62779 c -0.03123,0.8953 -0.06216,1.63465 -0.06874,1.64299 -0.0066,0.008 -1.046781,0.0491 -2.311564,0.0907 l -2.299599,0.0755 z"
|
||||
id="path326" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m -69.206112,-697.48987 v -7.6998 l 1.576131,0.006 c 1.768213,0.007 2.341875,0.0775 2.464015,0.30408 0.04595,0.0853 0.111916,0.37539 0.146584,0.64474 0.0771,0.59894 0.279998,0.82389 0.705297,0.78195 0.238075,-0.0236 0.407321,-0.14893 0.66979,-0.49652 0.616445,-0.81629 1.706047,-1.39599 3.223178,-1.71479 1.265084,-0.26583 2.457117,0.0214 3.791485,0.91403 1.031885,0.69017 1.608886,1.46381 1.835782,2.46139 0.245671,1.08011 0.363008,3.77579 0.365225,8.39044 l 0.0019,4.10829 h -2.268646 -2.268644 l -0.03096,-5.40019 c -0.03527,-6.15284 0.0054,-5.8655 -0.927383,-6.55024 -0.639027,-0.46908 -1.184426,-0.54753 -2.075423,-0.29848 -0.876779,0.24506 -1.328452,0.4894 -1.711902,0.92615 -0.645099,0.73472 -0.807998,1.91924 -0.896218,6.51685 -0.03272,1.70532 -0.08541,3.48427 -0.117083,3.95324 l -0.05759,0.85267 h -2.212808 -2.212808 z"
|
||||
id="path327" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m -51.164,-691.41789 c 0.02556,-0.8953 0.0748,-4.30821 0.109435,-7.58428 l 0.06296,-5.95649 0.685427,-0.0604 c 0.376986,-0.0332 1.255162,-0.0911 1.951503,-0.12877 l 1.266071,-0.0684 v 1.09848 c 0,0.60417 0.04111,1.09849 0.09136,1.09849 0.05025,0 0.526817,-0.37235 1.05904,-0.82743 1.591826,-1.36112 2.774664,-1.85976 4.411612,-1.85976 1.005287,0 1.833546,0.19101 2.791928,0.64382 1.382948,0.65341 1.964611,1.52333 2.24005,3.35018 0.161464,1.0709 0.314693,4.5222 0.406574,9.15765 l 0.0548,2.76471 h -2.256248 -2.25625 l -0.07254,-3.69488 c -0.127828,-6.51113 -0.323659,-7.76483 -1.302745,-8.33998 -0.301064,-0.17685 -0.534879,-0.21194 -1.395206,-0.20937 -0.867968,0.003 -1.104111,0.0403 -1.474163,0.23535 -1.412327,0.74443 -1.881391,3.13618 -1.883881,9.60591 l -7.94e-4,2.40297 h -2.26764 -2.267641 z"
|
||||
id="path328" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m -27.103604,-689.47829 c -1.268902,-0.17999 -2.264008,-0.5901 -3.396861,-1.39988 -3.287175,-2.34974 -4.191188,-6.8213 -2.203815,-10.90083 1.181968,-2.42626 4.025617,-3.94531 7.371458,-3.93774 1.417817,0.003 2.431925,0.24516 3.752518,0.89527 1.645216,0.80991 3.087562,2.40853 3.654865,4.05087 0.22803,0.66014 0.24073,0.83868 0.24073,3.38408 0,2.49208 -0.01573,2.72544 -0.216716,3.21426 -0.323254,0.78618 -0.931956,1.64783 -1.748471,2.47502 -1.196427,1.21206 -2.115912,1.71728 -3.770897,2.07195 -0.856907,0.18365 -2.860674,0.26363 -3.682811,0.147 z m 3.264817,-3.75102 c 0.768079,-0.38351 1.209874,-0.89315 1.562625,-1.80258 0.18431,-0.47517 0.241199,-0.87291 0.277654,-1.94122 0.05773,-1.69169 -0.139443,-2.96429 -0.566239,-3.65461 -0.379202,-0.61336 -1.370004,-1.40949 -2.024794,-1.62698 -0.654336,-0.21733 -1.58405,-0.15197 -2.315702,0.16277 -1.744374,0.75042 -2.686942,3.4253 -2.140024,6.07309 0.268668,1.30069 0.792109,2.11143 1.789274,2.77133 0.848132,0.56126 2.314006,0.56909 3.417206,0.0182 z"
|
||||
id="path329" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m -11.111657,-689.9208 c -0.136531,-0.0702 -0.287907,-0.19743 -0.336393,-0.28268 -0.211676,-0.37227 -5.461512,-14.62596 -5.461512,-14.82842 0,-0.0751 3.220553,-0.0472 3.962091,0.0343 l 0.603123,0.0663 0.976301,2.99723 c 0.536965,1.64849 1.328117,4.08339 1.7581161,5.41092 0.4299983,1.32749 0.8180542,2.37394 0.862346,2.32542 0.1640044,-0.17968 1.5627434,-4.38174 3.1514396,-9.46748 l 0.4762108,-1.52448 h 2.1749216 2.1749215 l -0.071743,0.33592 c -0.18751,0.87791 -1.8958243,5.53677 -4.3335205,11.81822 l -1.2072172,3.11076 -1.4041043,0.0674 c -2.0638995,0.099 -3.0456776,0.0802 -3.3249816,-0.0634 z"
|
||||
id="path330" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m 3.5870853,-689.63537 c -1.4620285,-0.31779 -2.5365396,-1.04336 -3.01888123,-2.03846 -0.82058757,-1.69291 -0.62904473,-3.81449 0.45304233,-5.01806 0.8526538,-0.94834 1.9824786,-1.36646 5.3752623,-1.98927 3.0503715,-0.55993 3.5029112,-0.70204 3.6587173,-1.149 0.24758,-0.7102 -0.4321929,-2.10476 -1.2260736,-2.51529 -0.7205218,-0.37261 -2.2616887,-0.27472 -3.2055866,0.2036 -0.588119,0.298 -1.139085,0.89842 -1.139085,1.24129 0,0.4322 -0.3153593,0.50946 -2.2479248,0.55081 -0.9805603,0.0209 -1.78251107,-0.003 -1.78211282,-0.0496 3.9952e-4,-0.0483 0.0943873,-0.47845 0.20886444,-0.95599 0.52910388,-2.20713 1.59649938,-3.36611 3.58045998,-3.88771 1.7551642,-0.46143 5.0773381,-0.42939 6.3902084,0.0616 0.86404,0.32316 1.938301,0.98631 2.487562,1.53556 1.201909,1.20193 1.4245,2.63147 1.556843,9.99861 l 0.07089,3.94594 -1.024117,-0.0495 c -1.585391,-0.0766 -2.642056,-0.22837 -2.840478,-0.40794 -0.0982,-0.0889 -0.208889,-0.33295 -0.245971,-0.54239 -0.160218,-0.90496 -0.241627,-0.92705 -1.2003859,-0.32557 -2.1862407,1.37152 -3.980153,1.7981 -5.8512298,1.39136 z m 4.6449518,-3.18696 c 0.7523839,-0.36809 1.4565153,-1.00171 1.7214888,-1.54908 0.2756351,-0.56938 0.3169731,-2.33704 0.05965,-2.55058 -0.1990943,-0.16524 -0.7608383,-0.0923 -2.4349726,0.3162 -2.0357261,0.4967 -2.9637874,1.03537 -3.2121593,1.86436 -0.2352818,0.78531 0.3603941,1.74382 1.3081359,2.10492 0.646105,0.24619 1.8542621,0.15843 2.5578571,-0.18582 z"
|
||||
id="path331" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m 23.604761,-689.5856 c -1.115776,-0.0954 -1.362785,-0.15965 -2.100752,-0.54629 -1.084981,-0.56845 -1.583947,-1.43388 -1.828968,-3.17219 -0.132381,-0.93919 -0.301732,-4.91659 -0.305213,-7.16833 l -0.0025,-1.63896 -1.031893,0.0739 -1.031893,0.0738 -0.113001,-0.29524 c -0.06455,-0.16865 -0.09849,-0.74801 -0.07915,-1.35118 0.04275,-1.33337 0.06892,-1.36326 1.315946,-1.50249 l 0.939991,-0.10493 v -2.24602 -2.24602 l 1.73116,-0.0686 c 0.952138,-0.0377 1.975332,-0.0958 2.273763,-0.12909 l 0.542603,-0.0605 v 2.38903 2.389 h 2.428792 2.428792 v 1.5503 1.5503 h -2.428832 -2.428792 l 8.46e-4,2.9714 c 9.26e-4,3.22305 0.109157,4.62966 0.399635,5.19348 0.478137,0.92806 1.693383,1.26567 2.953943,0.82066 0.820175,-0.28956 0.927607,-0.22868 1.087865,0.61635 0.135886,0.71652 0.133227,1.31617 -0.0088,1.97826 -0.09943,0.46363 -0.159315,0.55052 -0.468479,0.67969 -0.194963,0.0815 -0.922684,0.1923 -1.617158,0.24633 -1.375077,0.10694 -1.376055,0.10692 -2.657945,-0.003 z"
|
||||
id="path332" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m 32.079695,-697.48987 v -7.6998 h 2.222087 2.222087 v 7.6998 7.6998 h -2.222087 -2.222087 z"
|
||||
id="path333" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m 32.079695,-708.76501 v -1.715 l 1.937866,0.0367 c 2.759893,0.0522 2.833947,0.10782 2.901741,2.17892 l 0.03975,1.21441 h -2.439681 -2.439677 z"
|
||||
id="path334" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m 45.980657,-689.58809 c -2.618576,-0.35097 -4.739611,-2.16807 -5.63721,-4.82941 -0.309203,-0.91676 -0.343234,-1.14329 -0.386308,-2.5717 -0.08481,-2.81249 0.475806,-4.66577 1.916676,-6.33603 1.340954,-1.55448 4.225753,-2.53743 6.845692,-2.33262 2.487372,0.19447 4.275714,1.02188 5.55657,2.57085 1.21924,1.47447 2.086832,4.15592 1.955384,6.04346 -0.247872,3.55936 -2.729547,6.70978 -5.754957,7.30575 -0.975392,0.19214 -3.541673,0.2776 -4.495847,0.1497 z m 3.298414,-3.51483 c 0.737323,-0.27588 1.532163,-0.99589 1.853346,-1.67886 0.659937,-1.40332 0.616662,-4.06609 -0.09174,-5.64462 -0.319563,-0.71207 -0.813165,-1.20915 -1.619832,-1.63121 -0.462647,-0.24207 -0.672938,-0.28615 -1.373134,-0.28784 -0.698844,-0.003 -0.918051,0.0428 -1.416206,0.28742 -0.340432,0.16719 -0.784413,0.50636 -1.051116,0.80298 -0.861007,0.95766 -1.19861,1.96718 -1.20048,3.58976 -0.0028,2.42139 0.824682,4.20405 2.138931,4.60799 0.7551,0.23206 2.076368,0.21023 2.760228,-0.0456 z"
|
||||
id="path335" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ff4f1d;fill-opacity:1;stroke:none;stroke-width:0.0390626;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m -112.92438,-697.06836 v -7.7079 h 7.9133 7.913292 l -0.06052,4.15997 c -0.03328,2.28798 -0.08813,5.70124 -0.121864,7.58505 l -0.06135,3.42508 -2.701308,0.0611 c -1.48572,0.0336 -4.99186,0.0889 -7.79143,0.12282 l -5.09013,0.0617 z"
|
||||
id="path336" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.15625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m 58.899763,-697.33485 v -7.6481 h 2.067057 c 1.954237,0 2.067058,0.0591 2.067058,1.0826 v 1.08259 l 1.492697,-1.13853 c 1.855771,-1.41546 4.488783,-1.78869 6.237867,-0.88421 2.147154,1.11033 2.604722,2.74847 2.604722,9.32514 v 5.82864 H 71.302107 69.23505 v -5.14073 c 0,-6.44281 -0.267833,-7.26381 -2.366884,-7.25529 -2.87652,0.0116 -3.273755,0.81044 -3.656404,7.35232 l -0.295015,5.0437 h -2.008492 -2.008492 z"
|
||||
id="path337" /></g></svg>
|
||||
|
After Width: | Height: | Size: 20 KiB |
21
src/lib/assets/logo.svg
Normal file
21
src/lib/assets/logo.svg
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="194.20682mm"
|
||||
height="156.94582mm"
|
||||
viewBox="0 0 194.20682 156.94582"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
id="layer1"
|
||||
transform="translate(339.70607,-339.04412)"><path
|
||||
style="display:inline;fill:#fc871f;fill-opacity:1"
|
||||
d="m -278.45771,463.96495 c -1.28064,-2.48646 -3.23885,-5.54137 -4.33905,-8.89723 -0.56397,-1.93275 -0.62404,-4.70681 -0.69142,-31.92639 -0.0406,-16.39535 0.0641,-31.12031 0.23261,-32.72215 0.36067,-3.4284 1.17076,-5.44162 3.00784,-7.47505 2.00752,-2.22208 3.72807,-2.99588 6.6784,-3.00352 2.14373,-0.006 2.81842,0.16591 5.11528,1.30002 3.10182,1.53157 4.20074,2.4493 30.31919,25.32014 10.52502,9.21632 19.7739,17.14059 20.55306,17.60949 1.05397,0.63429 1.92602,0.85208 3.40623,0.85073 3.02423,-0.003 2.99988,0.0144 13.45059,-9.46438 2.56708,-2.32834 5.57881,-5.02709 6.69274,-5.99723 1.11392,-0.97013 6.27724,-5.57388 11.47404,-10.23055 16.89088,-15.13532 19.52862,-17.39767 21.9878,-18.85864 5.26743,-3.12932 9.4068,-2.57736 12.66024,1.68819 2.4859,3.25924 2.35251,0.41054 2.40634,51.38989 0.0266,25.22361 -0.0666,47.38153 -0.20716,49.23981 -0.19301,2.55154 -0.47299,3.80772 -1.14364,5.13118 -1.4785,2.91769 -3.85973,5.25546 -6.86747,6.74216 -2.56901,1.26983 -2.84441,1.32852 -6.23416,1.32852 -3.11452,0 -3.79356,-0.11582 -5.57581,-0.95105 -2.83901,-1.33045 -5.68121,-4.13009 -7.0663,-6.96047 l -1.13486,-2.31904 -0.17638,-24.40437 c -0.17409,-24.08605 -0.18604,-24.41737 -0.91575,-25.4 -1.06111,-1.42888 -2.89266,-1.8199 -4.5005,-0.9608 -0.69043,0.3689 -7.25516,6.23981 -14.58829,13.04647 -17.7355,16.46219 -19.0948,17.67837 -20.50871,18.34931 -1.69478,0.80423 -4.61682,0.52492 -6.18502,-0.5912 -1.17058,-0.83313 -12.04421,-10.15478 -15.57956,-13.35592 -0.97703,-0.88467 -3.33991,-2.95786 -5.25084,-4.6071 -3.31016,-2.85684 -9.71138,-8.46422 -11.40537,-9.99095 l -0.79375,-0.71537 0.14718,28.63126 c 0,0 72.47879,53.24691 -24.9675,-1.79576 z"
|
||||
id="path1" /><path
|
||||
style="fill:#ef3826;fill-opacity:1"
|
||||
d="m -283.32309,495.62791 c -23.07343,-0.24221 -23.31134,-0.25797 -29.11453,-1.92799 -3.01241,-0.86691 -8.80358,-3.70689 -11.27852,-5.53097 -7.53433,-5.55295 -12.56531,-12.73221 -14.904,-21.26813 l -0.96655,-3.52778 -0.10018,-53.94876 c -0.0624,-33.61888 0.0292,-54.8822 0.24309,-56.4258 0.64175,-4.63096 4.0281,-9.75053 7.96653,-12.04402 7.89424,-4.5971 17.90951,-0.58134 21.59624,8.65931 0.58949,1.47753 0.63977,4.63823 0.80514,50.61205 l 0.17639,49.03611 0.82472,2.46945 c 2.01576,6.03573 5.22147,9.3852 10.86411,11.35132 2.68451,0.93539 3.05708,0.97074 11.41811,1.08342 5.68115,0.0766 8.46749,-10e-4 8.13074,-0.22683 -0.28178,-0.18881 20.77607,-0.005 23.97206,0.56189 l 3.43959,0.22437 c 6.74878,0.44023 11.73086,2.51223 16.23992,6.75403 3.13175,2.94613 5.6233,6.40219 9.9072,13.74239 2.4559,4.20805 2.86884,5.14797 2.86884,6.53 0,1.7713 -1.0489,3.29309 -2.70335,3.92211 -0.93451,0.3553 -22.75786,0.33834 -59.38553,-0.0462 z"
|
||||
id="path1-4" /></g></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
189
src/lib/components/Footer.svelte
Normal file
189
src/lib/components/Footer.svelte
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { t } from '$lib/translations';
|
||||
import logo from '$lib/assets/logo.svg';
|
||||
|
||||
import ArrowTopIcon from './icons/ArrowTopIcon.svelte';
|
||||
import LinkedInIcon from './icons/LinkedInIcon.svelte';
|
||||
import MailIcon from './icons/MailIcon.svelte';
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
<div id="content-container">
|
||||
<section class="footer-block">
|
||||
<img
|
||||
src={logo}
|
||||
style="height: 80px; width: auto; margin-lft: -5px;"
|
||||
alt={$t('footer.logoAlt')}
|
||||
/>
|
||||
<p>
|
||||
{$t('footer.tagline')}
|
||||
</p>
|
||||
</section>
|
||||
<section class="footer-block" aria-labelledby="contact-title">
|
||||
<h1 id="contact-title">{$t('footer.contactHeading')}</h1>
|
||||
<p>{$t('footer.contactLead')}</p>
|
||||
|
||||
<address class="contact-container">
|
||||
<div class="contact-item">
|
||||
<MailIcon size={30} />
|
||||
|
||||
<a href="mailto:leo@leomurca.xyz" aria-label={$t('footer.emailAria')}
|
||||
>leo@leomurca.xyz</a
|
||||
>
|
||||
</div>
|
||||
</address>
|
||||
|
||||
<address class="contact-container">
|
||||
<div class="contact-item">
|
||||
<LinkedInIcon size={30} />
|
||||
|
||||
<a
|
||||
href="https://linkedin.com/in/leonardoamurca"
|
||||
target="_blank"
|
||||
aria-label={$t('footer.linkedinAria')}>/leonardoamurca</a
|
||||
>
|
||||
</div>
|
||||
</address>
|
||||
</section>
|
||||
<section class="footer-block">
|
||||
<h1>{$t('footer.resourcesHeading')}</h1>
|
||||
<nav class="social-container" aria-label={$t('footer.resourcesNavAria')}>
|
||||
<a
|
||||
href="https://git.leomurca.xyz/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={$t('footer.gitAria')}>{$t('footer.gitRepos')}</a
|
||||
>
|
||||
<a href={resolve('/privacy-policy')}>{$t('footer.privacyPolicy')}</a>
|
||||
</nav>
|
||||
<button
|
||||
class="back-to-top-button"
|
||||
aria-label={$t('footer.backToTop')}
|
||||
onclick={() => scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
>
|
||||
<ArrowTopIcon size={30} />{$t('footer.backToTop')}</button
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
<section class="credits-container">
|
||||
Copyright © {new Date().getFullYear()}
|
||||
|
||||
<a href="https://leomurca.xyz" target="_blank" rel="noreferrer"
|
||||
>Leonardo Murça</a
|
||||
> | {$t('footer.copyright')}
|
||||
</section>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
border-top: 2px solid #e6e7eb;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
#content-container {
|
||||
width: 85%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
padding: 60px 0 60px 30px;
|
||||
scroll-margin-top: 100px;
|
||||
color: var(--color-font-light);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.footer-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contact-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
letter-spacing: 0.1rem;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.contact-item:hover {
|
||||
font-weight: 700;
|
||||
}
|
||||
.social-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.social-container a:hover {
|
||||
font-weight: 700;
|
||||
}
|
||||
.back-to-top-button {
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
padding: 13px;
|
||||
text-decoration: none;
|
||||
color: var(--color-font-light);
|
||||
font-size: 1rem;
|
||||
width: 40%;
|
||||
margin-top: 30px;
|
||||
border: 1px solid var(--color-font-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.back-to-top-button:hover {
|
||||
background-color: var(--color-font-light);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.credits-container {
|
||||
background-color: #f9f9f9;
|
||||
color: var(--color-font-light);
|
||||
margin: 0 auto;
|
||||
padding: 20px 30px;
|
||||
padding-left: 9%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.credits-container a {
|
||||
color: var(--color-primary);
|
||||
border-bottom: 1px solid var(--color-primary);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.credits-container a:hover {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
#content-container {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 60px 30px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.back-to-top-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
96
src/lib/components/Head.svelte
Normal file
96
src/lib/components/Head.svelte
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<script>
|
||||
/* eslint-disable svelte/no-at-html-tags -- JSON-LD is serialized and script-safe */
|
||||
import { page } from '$app/state';
|
||||
import { locale } from '$lib/translations';
|
||||
import { isDevelopment } from '$lib/utils/env';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {string} title Document `<title>` and primary SEO title.
|
||||
* @property {string} description Meta description (search snippets).
|
||||
* @property {string} keywords Comma-separated keywords (legacy / some crawlers).
|
||||
* @property {string} ogTitle Open Graph title (share cards).
|
||||
* @property {string} ogDescription Open Graph description.
|
||||
* @property {string} ogImage Absolute URL to preview image (HTTPS recommended).
|
||||
* @property {string} [twitterDescription] Twitter description; falls back to `ogDescription` when omitted.
|
||||
* @property {'website' | 'article'} [ogType] `og:type` for Facebook / LinkedIn previews.
|
||||
* @property {string} [ogImageAlt] Descriptive alt text for the preview image.
|
||||
* @property {string} [robots] Robots directive; default index, follow.
|
||||
* @property {string} [author] Author meta and `article:author` when `ogType` is `article`.
|
||||
* @property {string | null} [jsonLd] Serialized JSON-LD (`JSON.stringify`); injected as `application/ld+json`.
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
title,
|
||||
description,
|
||||
keywords,
|
||||
ogTitle,
|
||||
ogDescription,
|
||||
ogImage,
|
||||
twitterDescription: twitterDescriptionProp,
|
||||
ogType = 'website',
|
||||
ogImageAlt = '',
|
||||
robots = 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
|
||||
author = 'Leonardo Murça',
|
||||
jsonLd = null,
|
||||
} = $props();
|
||||
|
||||
const twitterDescription = $derived(twitterDescriptionProp ?? ogDescription);
|
||||
|
||||
const siteName = 'Leonardo Murça';
|
||||
|
||||
const canonicalUrl = $derived(
|
||||
`${page.url.origin}${page.url.pathname || '/'}`,
|
||||
);
|
||||
|
||||
const ogLocale = $derived($locale === 'pt-BR' ? 'pt_BR' : 'en_US');
|
||||
|
||||
const alternateLocale = $derived($locale === 'pt-BR' ? 'en_US' : 'pt_BR');
|
||||
|
||||
const imageAlt = $derived(ogImageAlt || ogTitle);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title}</title>
|
||||
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
|
||||
<meta name="description" content={description} />
|
||||
<meta name="keywords" content={keywords} />
|
||||
<meta name="author" content={author} />
|
||||
<meta name="robots" content={robots} />
|
||||
<meta name="googlebot" content={robots} />
|
||||
|
||||
<meta property="og:site_name" content={siteName} />
|
||||
<meta property="og:title" content={ogTitle} />
|
||||
<meta property="og:description" content={ogDescription} />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
<meta property="og:type" content={ogType} />
|
||||
<meta property="og:locale" content={ogLocale} />
|
||||
<meta property="og:locale:alternate" content={alternateLocale} />
|
||||
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:secure_url" content={ogImage} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content={imageAlt} />
|
||||
|
||||
{#if ogType === 'article'}
|
||||
<meta property="article:author" content={author} />
|
||||
{/if}
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={ogTitle} />
|
||||
<meta name="twitter:description" content={twitterDescription} />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
<meta name="twitter:image:alt" content={imageAlt} />
|
||||
|
||||
{#if jsonLd}
|
||||
{@html `<script type="application/ld+json">${jsonLd.replace(/</g, '\\u003c')}</script>`}
|
||||
{/if}
|
||||
|
||||
{#if !isDevelopment()}
|
||||
<script async src="https://hk.leomurca.xyz/hk.js"></script>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
238
src/lib/components/Header.svelte
Normal file
238
src/lib/components/Header.svelte
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { t } from '$lib/translations';
|
||||
import logo from '$lib/assets/logo.svg';
|
||||
|
||||
let navOpen = $state(false);
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<div class="nav-container">
|
||||
<div class="logo">
|
||||
<a href={resolve('/')} aria-label={$t('header.homeAria')}
|
||||
><img src={logo} alt={$t('header.logoAlt')} width="45" /></a
|
||||
>
|
||||
</div>
|
||||
|
||||
<nav class:open={navOpen}>
|
||||
<ul>
|
||||
<li>
|
||||
<a onclick={() => (navOpen = !navOpen)} href={resolve('/')}
|
||||
>{$t('header.navHome')}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a onclick={() => (navOpen = !navOpen)} href="#featured-works"
|
||||
>{$t('header.navWorks')}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a onclick={() => (navOpen = !navOpen)} href="#about"
|
||||
>{$t('header.navAbout')}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a onclick={() => (navOpen = !navOpen)} href="#contact"
|
||||
>{$t('header.navContact')}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="button-group">
|
||||
<a
|
||||
class="primary-button"
|
||||
href="https://git.leomurca.xyz/leomurca/resume/releases/download/latest/leonardo-murca-resume.pdf"
|
||||
target="_blank">{$t('header.resume')}</a
|
||||
>
|
||||
<button
|
||||
class="secondary-button nav-toggle"
|
||||
aria-controls="primary-navigation"
|
||||
aria-expanded={navOpen}
|
||||
onclick={() => (navOpen = !navOpen)}
|
||||
aria-label={$t('header.menuAria')}
|
||||
style="margin-left: 20px;"
|
||||
class:open={navOpen}
|
||||
>
|
||||
<span class="hamburger"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
/* ================================
|
||||
LAYOUT / STRUCTURE
|
||||
================================= */
|
||||
header {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
width: min-content;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
color: var(--color-dark-background);
|
||||
border-radius: 20px;
|
||||
padding: 5px 15px;
|
||||
border-color: rgba(33, 33, 33, 0.1);
|
||||
box-shadow:
|
||||
rgba(0, 0, 0, 0.18) 0px 0.602187px 0.602187px -1.25px,
|
||||
rgba(0, 0, 0, 0.16) 0px 2.28853px 2.28853px -2.5px,
|
||||
rgba(0, 0, 0, 0.06) 0px 10px 10px -3.75px,
|
||||
rgb(205, 204, 203) 0px 3px 0px 0px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
NAV LINKS
|
||||
================================= */
|
||||
li a:hover {
|
||||
border-bottom: 2px solid var(--color-dark-background);
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
BUTTON GROUP
|
||||
================================= */
|
||||
.button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
NAV TOGGLE (HAMBURGER)
|
||||
================================= */
|
||||
.nav-toggle {
|
||||
border: none;
|
||||
display: none;
|
||||
width: 30px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Hamburger lines */
|
||||
.hamburger,
|
||||
.hamburger::before,
|
||||
.hamburger::after {
|
||||
content: '';
|
||||
background-color: white;
|
||||
height: 3px;
|
||||
width: 20px;
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.hamburger {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.hamburger::before {
|
||||
top: -8px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.hamburger::after {
|
||||
bottom: -8px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Hamburger → Cross (open state) */
|
||||
.nav-toggle.open .hamburger {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.nav-toggle.open .hamburger::before {
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.nav-toggle.open .hamburger::after {
|
||||
bottom: 50%;
|
||||
transform: translateY(50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
/* ================================
|
||||
MOBILE / RESPONSIVE
|
||||
================================= */
|
||||
@media (max-width: 810px) {
|
||||
header {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.nav-container {
|
||||
width: 90%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav-toggle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
nav {
|
||||
position: absolute;
|
||||
top: 120%;
|
||||
left: 5%;
|
||||
right: 0;
|
||||
width: 90%;
|
||||
|
||||
background-color: white;
|
||||
border-radius: 20px;
|
||||
border-color: rgba(33, 33, 33, 0.1);
|
||||
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
|
||||
flex: unset;
|
||||
|
||||
box-shadow:
|
||||
rgba(0, 0, 0, 0.18) 0px 0.602187px 0.602187px -1.25px,
|
||||
rgba(0, 0, 0, 0.16) 0px 2.28853px 2.28853px -2.5px,
|
||||
rgba(0, 0, 0, 0.06) 0px 10px 10px -3.75px,
|
||||
rgb(205, 204, 203) 0px 3px 0px 0px;
|
||||
}
|
||||
|
||||
nav.open {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
nav li {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
li a:hover {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
202
src/lib/components/LanguageSwitch.svelte
Normal file
202
src/lib/components/LanguageSwitch.svelte
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
<script>
|
||||
import { get } from 'svelte/store';
|
||||
import { page } from '$app/state';
|
||||
import {
|
||||
locale,
|
||||
loadTranslations,
|
||||
setLocale,
|
||||
setRoute,
|
||||
SUPPORTED_LOCALES,
|
||||
t,
|
||||
} from '$lib/translations';
|
||||
|
||||
async function toggleLocale() {
|
||||
const current = get(locale);
|
||||
const next =
|
||||
current === SUPPORTED_LOCALES.EN_US
|
||||
? SUPPORTED_LOCALES.PT_BR
|
||||
: SUPPORTED_LOCALES.EN_US;
|
||||
const route = page.url.pathname;
|
||||
await loadTranslations(next, route);
|
||||
await setLocale(next);
|
||||
await setRoute(route);
|
||||
}
|
||||
|
||||
const isEn = $derived($locale === SUPPORTED_LOCALES.EN_US);
|
||||
</script>
|
||||
|
||||
<div class="lang-float">
|
||||
<button
|
||||
type="button"
|
||||
class="lang-toggle"
|
||||
class:lang-toggle--en={isEn}
|
||||
role="switch"
|
||||
aria-checked={isEn}
|
||||
aria-label={$t('header.langToggleAria')}
|
||||
title={isEn ? 'English' : 'Português'}
|
||||
onclick={toggleLocale}
|
||||
>
|
||||
<span class="lang-toggle__inner">
|
||||
<svg
|
||||
class="lang-toggle__icon"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="9.25"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
/>
|
||||
<ellipse
|
||||
cx="12"
|
||||
cy="12"
|
||||
rx="3.25"
|
||||
ry="9.25"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
/>
|
||||
<path
|
||||
d="M3.25 12h17.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span class="lang-toggle__code" aria-hidden="true"
|
||||
>{isEn ? 'EN' : 'PT'}</span
|
||||
>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.lang-float {
|
||||
position: fixed;
|
||||
z-index: 40;
|
||||
left: max(12px, env(safe-area-inset-right, 0px));
|
||||
top: auto;
|
||||
bottom: max(16px, env(safe-area-inset-bottom, 0px));
|
||||
pointer-events: none;
|
||||
padding: 0;
|
||||
filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.18));
|
||||
}
|
||||
|
||||
@media (min-width: 901px) {
|
||||
.lang-float {
|
||||
top: 30px;
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.lang-toggle {
|
||||
pointer-events: auto;
|
||||
box-sizing: border-box;
|
||||
min-width: 54px;
|
||||
min-height: 54px;
|
||||
padding: 7px 8px 8px;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
cursor: pointer;
|
||||
color: #ffffff;
|
||||
/* WCAG-friendly vs white icon: dark green = PT */
|
||||
background: #0b5c3a;
|
||||
border: 2px solid rgba(255, 255, 255, 0.35);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.12);
|
||||
transition:
|
||||
background-color 0.22s ease,
|
||||
border-color 0.22s ease,
|
||||
box-shadow 0.22s ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.lang-toggle__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.lang-toggle__code {
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.lang-toggle:hover .lang-toggle__code {
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
/* English: deep blue, same white icon contrast */
|
||||
.lang-toggle--en {
|
||||
background: #124080;
|
||||
border-color: rgba(255, 255, 255, 0.38);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.18),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
.lang-toggle:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.lang-toggle:focus-visible {
|
||||
outline: 3px solid #ffffff;
|
||||
outline-offset: 3px;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||
0 0 0 2px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.lang-toggle:hover {
|
||||
filter: brightness(1.06);
|
||||
}
|
||||
|
||||
.lang-toggle:active {
|
||||
filter: brightness(0.94);
|
||||
}
|
||||
|
||||
.lang-toggle__icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.lang-toggle {
|
||||
min-width: 58px;
|
||||
min-height: 58px;
|
||||
padding: 8px 9px 9px;
|
||||
}
|
||||
|
||||
.lang-toggle__icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.lang-toggle__code {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.lang-toggle {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
14
src/lib/components/icons/ArrowBackIcon.svelte
Normal file
14
src/lib/components/icons/ArrowBackIcon.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script>
|
||||
let { size = 20, color = 'currentColor' } = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill={color}
|
||||
width={size}
|
||||
><path d="M0 0h24v24H0z" fill="none" /><path
|
||||
d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"
|
||||
/></svg
|
||||
>
|
||||
13
src/lib/components/icons/ArrowTopIcon.svelte
Normal file
13
src/lib/components/icons/ArrowTopIcon.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script>
|
||||
let { size = 20, color = 'currentColor' } = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={size}
|
||||
viewBox="0 -960 960 960"
|
||||
width={size}
|
||||
fill={color}
|
||||
>
|
||||
<path d="M480-528 296-344l-56-56 240-240 240 240-56 56-184-184Z" />
|
||||
</svg>
|
||||
24
src/lib/components/icons/LinkedInIcon.svelte
Normal file
24
src/lib/components/icons/LinkedInIcon.svelte
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<script>
|
||||
let { size = 20, color = 'currentColor' } = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon"
|
||||
>
|
||||
<!-- Outer shape -->
|
||||
<rect x="2" y="2" width="20" height="20" rx="4" ry="4" />
|
||||
|
||||
<!-- "in" symbol -->
|
||||
<line x1="8" y1="11" x2="8" y2="16" />
|
||||
<line x1="8" y1="8" x2="8" y2="8" />
|
||||
<path d="M12 16v-3a2 2 0 0 1 4 0v3" />
|
||||
</svg>
|
||||
18
src/lib/components/icons/MailIcon.svelte
Normal file
18
src/lib/components/icons/MailIcon.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script>
|
||||
let { size = 20, strokeWidth = 2 } = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width={strokeWidth}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" ry="2" />
|
||||
<polyline points="3,7 12,13 21,7" />
|
||||
</svg>
|
||||
7
src/lib/config/imageCdn.js
Normal file
7
src/lib/config/imageCdn.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* CDN base for `/t/...` image paths — set `PUBLIC_IMAGE_BASE_URL` in `.env` (see `.env.example`).
|
||||
* Uses `$env/static/public` so Vite/SvelteKit always inlines the value (avoids empty base → `/t/...` 404s on localhost).
|
||||
*/
|
||||
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
|
||||
|
||||
export const imageBaseUrl = PUBLIC_IMAGE_BASE_URL.replace(/\/$/, '');
|
||||
1
src/lib/index.js
Normal file
1
src/lib/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
// place files you want to import through the `$lib` alias in this folder.
|
||||
400
src/lib/sections/About.svelte
Normal file
400
src/lib/sections/About.svelte
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
<script>
|
||||
import { imageBaseUrl } from '$lib/config/imageCdn.js';
|
||||
import { locale, t } from '$lib/translations';
|
||||
|
||||
/** Career start year for experience calculation. */
|
||||
const START_YEAR = 2018;
|
||||
|
||||
/** Full calendar years elapsed since `START_YEAR` (e.g. 2026 − 2018 → 8). */
|
||||
const years = $derived(Math.max(0, new Date().getFullYear() - START_YEAR));
|
||||
|
||||
const yearNoun = $derived(
|
||||
years === 1
|
||||
? $locale === 'pt-BR'
|
||||
? 'ano'
|
||||
: 'year'
|
||||
: $locale === 'pt-BR'
|
||||
? 'anos'
|
||||
: 'years',
|
||||
);
|
||||
|
||||
const aboutPayload = $derived({ years, sinceYear: START_YEAR, yearNoun });
|
||||
|
||||
/** Portrait for the About section (add `leomurca/leonardo-about.webp` to your image CDN). */
|
||||
const portraitSrc = `${imageBaseUrl}/t/f_webp,w_960,h_1200,c_fill/leomurca/me.webp`;
|
||||
</script>
|
||||
|
||||
<section id="about" class="about" aria-labelledby="about-title">
|
||||
<div class="about__decor" aria-hidden="true">
|
||||
<!-- Very light motifs: mobile / BLE / streaming / UI layers / code -->
|
||||
<svg
|
||||
class="about__deco about__deco--phone"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="14"
|
||||
y="6"
|
||||
width="36"
|
||||
height="52"
|
||||
rx="5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M24 10h16"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<circle cx="32" cy="52" r="2.5" fill="currentColor" />
|
||||
</svg>
|
||||
<svg
|
||||
class="about__deco about__deco--ble"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M32 12 L44 24 36 32 44 40 32 52 M32 12 L20 24 28 32 20 40 32 52"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
class="about__deco about__deco--stream"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 40c10-18 38-18 48 0M14 32c8-12 28-12 36 0M20 24c6-8 22-8 28 0"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<polygon points="30,22 38,28 30,34" fill="currentColor" opacity="0.35" />
|
||||
</svg>
|
||||
<svg
|
||||
class="about__deco about__deco--layers"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="10"
|
||||
y="36"
|
||||
width="44"
|
||||
height="10"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<rect
|
||||
x="16"
|
||||
y="22"
|
||||
width="32"
|
||||
height="10"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<rect
|
||||
x="22"
|
||||
y="8"
|
||||
width="20"
|
||||
height="10"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
class="about__deco about__deco--code"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M22 20 L12 32 22 44"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M42 20 L52 32 42 44"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M36 16 L28 48"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
opacity="0.5"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
class="about__deco about__deco--web"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<ellipse
|
||||
cx="32"
|
||||
cy="34"
|
||||
rx="22"
|
||||
ry="14"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path d="M10 34h44" stroke="currentColor" stroke-width="1.5" />
|
||||
<path
|
||||
d="M32 20v28"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
opacity="0.45"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="about__inner">
|
||||
<div class="about__copy">
|
||||
<p class="about__eyebrow">{$t('about.eyebrow')}</p>
|
||||
<h2 id="about-title" class="about__title">{$t('about.title')}</h2>
|
||||
<p class="about__subtitle">{$t('about.subtitle', aboutPayload)}</p>
|
||||
<p class="about__body">{$t('about.p1', aboutPayload)}</p>
|
||||
<p class="about__body">{$t('about.p2')}</p>
|
||||
<p class="about__body">{$t('about.p3')}</p>
|
||||
<p class="about__skills">{$t('about.skills')}</p>
|
||||
</div>
|
||||
|
||||
<figure class="about__figure">
|
||||
<div class="about__frame">
|
||||
<img
|
||||
class="about__photo"
|
||||
src={portraitSrc}
|
||||
alt={$t('about.imageAlt')}
|
||||
width="640"
|
||||
height="800"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.about {
|
||||
position: relative;
|
||||
padding: clamp(3.5rem, 8vw, 5.5rem) clamp(1.25rem, 5vw, 5rem);
|
||||
background: var(--color-light-background);
|
||||
color: var(--color-font-dark);
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.about__decor {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
color: var(--color-dark-background);
|
||||
opacity: 0.055;
|
||||
}
|
||||
|
||||
.about__deco {
|
||||
position: absolute;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.about__deco--phone {
|
||||
top: 6%;
|
||||
right: 6%;
|
||||
width: min(140px, 22vw);
|
||||
height: min(140px, 22vw);
|
||||
transform: rotate(-11deg);
|
||||
}
|
||||
|
||||
.about__deco--ble {
|
||||
bottom: 12%;
|
||||
left: 4%;
|
||||
width: min(100px, 16vw);
|
||||
height: min(100px, 16vw);
|
||||
transform: rotate(8deg);
|
||||
}
|
||||
|
||||
.about__deco--stream {
|
||||
top: 38%;
|
||||
right: 2%;
|
||||
width: min(180px, 28vw);
|
||||
height: min(180px, 28vw);
|
||||
transform: rotate(4deg);
|
||||
}
|
||||
|
||||
.about__deco--layers {
|
||||
top: 18%;
|
||||
left: 8%;
|
||||
width: min(120px, 18vw);
|
||||
height: min(120px, 18vw);
|
||||
transform: rotate(-6deg);
|
||||
}
|
||||
|
||||
.about__deco--code {
|
||||
bottom: 6%;
|
||||
right: 22%;
|
||||
width: min(110px, 17vw);
|
||||
height: min(110px, 17vw);
|
||||
transform: rotate(7deg);
|
||||
}
|
||||
|
||||
.about__deco--web {
|
||||
bottom: 28%;
|
||||
left: 14%;
|
||||
width: min(95px, 15vw);
|
||||
height: min(95px, 15vw);
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
|
||||
.about__inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(1180px, 100%);
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.82fr);
|
||||
gap: clamp(2rem, 5vw, 4rem);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.about__decor {
|
||||
opacity: 0.04;
|
||||
}
|
||||
|
||||
.about__deco--stream {
|
||||
width: min(140px, 40vw);
|
||||
height: min(140px, 40vw);
|
||||
right: -4%;
|
||||
}
|
||||
|
||||
.about__deco--layers {
|
||||
left: -2%;
|
||||
}
|
||||
|
||||
.about__inner {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2.25rem;
|
||||
}
|
||||
|
||||
.about__figure {
|
||||
justify-self: center;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.about__title {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.about__eyebrow {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.about__title {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: clamp(2rem, 4.2vw, 3.1rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
max-width: 18ch;
|
||||
}
|
||||
|
||||
.about__subtitle {
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: clamp(1.05rem, 1.9vw, 1.2rem);
|
||||
font-weight: 600;
|
||||
color: #3f4a57;
|
||||
max-width: 36ch;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.about__body {
|
||||
margin: 0 0 1.1rem;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.75;
|
||||
color: #3a4450;
|
||||
max-width: 58ch;
|
||||
}
|
||||
|
||||
.about__body:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.about__skills {
|
||||
margin: 1.75rem 0 0;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-font-light);
|
||||
line-height: 1.6;
|
||||
max-width: 52ch;
|
||||
}
|
||||
|
||||
.about__figure {
|
||||
margin: 0;
|
||||
justify-self: end;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.about__frame {
|
||||
position: relative;
|
||||
border-radius: clamp(1rem, 2vw, 1.35rem);
|
||||
padding: 3px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(254, 147, 53, 0.95) 0%,
|
||||
rgba(241, 69, 47, 0.85) 45%,
|
||||
rgba(16, 22, 29, 0.35) 100%
|
||||
);
|
||||
box-shadow:
|
||||
0 24px 50px rgba(16, 22, 29, 0.14),
|
||||
0 4px 14px rgba(16, 22, 29, 0.08);
|
||||
}
|
||||
|
||||
.about__photo {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 4 / 5;
|
||||
object-fit: cover;
|
||||
border-radius: calc(clamp(1rem, 2vw, 1.35rem) - 3px);
|
||||
background: linear-gradient(145deg, #e8ecf1 0%, #d4dae3 100%);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.about__figure {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.about__photo {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
52
src/lib/sections/Brands.svelte
Normal file
52
src/lib/sections/Brands.svelte
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import { t } from '$lib/translations';
|
||||
import arctouch from '$lib/assets/arctouch-logo-white.svg';
|
||||
import disney from '$lib/assets/disney-logo-white.svg';
|
||||
import ciandt from '$lib/assets/ciandt-logo-white.svg';
|
||||
import iebt from '$lib/assets/iebt-logo-white.svg';
|
||||
</script>
|
||||
|
||||
<section id="brands">
|
||||
<h1>{$t('brands.title')}</h1>
|
||||
<div class="logos-container">
|
||||
<img src={disney} alt="Arctouch" width={150} />
|
||||
<img src={arctouch} alt="Arctouch" width={200} />
|
||||
<img src={iebt} alt="Arctouch" width={100} />
|
||||
<img src={ciandt} alt="Arctouch" width={120} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* =========================
|
||||
LAYOUT / STRUCTURE
|
||||
========================= */
|
||||
section {
|
||||
background-color: var(--color-dark-background);
|
||||
padding: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 3px;
|
||||
color: #f4f4f5;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.logos-container {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
MOBILE / RESPONSIVE
|
||||
========================= */
|
||||
@media (max-width: 810px) {
|
||||
.logos-container {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 70px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
127
src/lib/sections/Contact.svelte
Normal file
127
src/lib/sections/Contact.svelte
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<script>
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
const MAIL = 'leo@leomurca.xyz';
|
||||
const LINKEDIN = 'https://linkedin.com/in/leonardoamurca';
|
||||
const GIT = 'https://git.leomurca.xyz/';
|
||||
</script>
|
||||
|
||||
<section
|
||||
id="contact"
|
||||
class="contact"
|
||||
aria-labelledby="contact-strip-title"
|
||||
>
|
||||
<div class="contact__inner">
|
||||
<h2 id="contact-strip-title" class="contact__title">
|
||||
{$t('contact.title')}
|
||||
</h2>
|
||||
<p class="contact__lead">{$t('contact.lead')}</p>
|
||||
|
||||
<a
|
||||
class="primary-button contact__cta"
|
||||
href={`mailto:${MAIL}?subject=${encodeURIComponent($t('contact.mailSubject'))}`}
|
||||
>{$t('contact.cta')}</a
|
||||
>
|
||||
|
||||
<p class="contact__links-intro">{$t('contact.linksIntro')}</p>
|
||||
<ul class="contact__links">
|
||||
<li>
|
||||
<a href={`mailto:${MAIL}`} aria-label={$t('footer.emailAria')}
|
||||
>{$t('contact.emailLink')}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={LINKEDIN}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={$t('footer.linkedinAria')}>{$t('contact.linkedinLink')}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={GIT}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={$t('footer.gitAria')}>{$t('contact.gitLink')}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.contact {
|
||||
scroll-margin-top: 100px;
|
||||
margin: 0;
|
||||
padding: 72px 80px 80px;
|
||||
background: var(--color-dark-background);
|
||||
color: #f4f4f5;
|
||||
}
|
||||
|
||||
.contact__inner {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.contact__title {
|
||||
margin: 0 0 1rem;
|
||||
font-size: clamp(2rem, 4vw, 2.5rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.contact__lead {
|
||||
margin: 0 0 1.75rem;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.55;
|
||||
color: #a1a1aa;
|
||||
max-width: 52ch;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.contact__cta {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.contact__links-intro {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.contact__links {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem 1.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.contact__links a {
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.contact__links a:hover {
|
||||
color: #ffb366;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.contact {
|
||||
padding: 56px 24px 64px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
171
src/lib/sections/FeaturedWork.svelte
Normal file
171
src/lib/sections/FeaturedWork.svelte
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { imageBaseUrl } from '$lib/config/imageCdn.js';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
/** @type {readonly { titleKey: string; altKey: string; image: string; route: '/embroidery-viewer' | '/entrepreneurship-alagoas' | '/implant-file' }[]} */
|
||||
const works = [
|
||||
{
|
||||
titleKey: 'featured.workEmbroidery',
|
||||
image: 'embroidery-viewer-square-thumb.webp',
|
||||
altKey: 'featured.altEmbroidery',
|
||||
route: '/embroidery-viewer',
|
||||
},
|
||||
{
|
||||
titleKey: 'featured.workAlagoas',
|
||||
image: 'empreendedorismo-square-thumb.webp',
|
||||
altKey: 'featured.altAlagoas',
|
||||
route: '/entrepreneurship-alagoas',
|
||||
},
|
||||
{
|
||||
titleKey: 'featured.workImplant',
|
||||
image: 'implant-file-thumb.webp',
|
||||
altKey: 'featured.altImplant',
|
||||
route: '/implant-file',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<section id="featured-works" class="featured">
|
||||
<header class="featured__header">
|
||||
<h1 class="featured__title">{$t('featured.sectionTitle')}</h1>
|
||||
</header>
|
||||
|
||||
<ul class="featured__grid">
|
||||
{#each works as work (work.route)}
|
||||
<li class="featured__item">
|
||||
<a href={resolve(work.route)}>
|
||||
<img
|
||||
class="featured__image"
|
||||
src={`${imageBaseUrl}/t/f_webp/leomurca/${work.image}`}
|
||||
alt={$t(work.altKey)}
|
||||
/>
|
||||
<p class="featured__label">{$t(work.titleKey)}</p>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* =========================
|
||||
Layout / Section
|
||||
========================== */
|
||||
.featured {
|
||||
padding: 80px;
|
||||
margin: 0;
|
||||
background-color: var(--color-light-background);
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Header
|
||||
========================== */
|
||||
.featured__header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.featured__title {
|
||||
font-size: clamp(3.5rem, 4vw, 3.5rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0 0 1rem;
|
||||
color: var(--color-font-dark);
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Grid
|
||||
========================== */
|
||||
.featured__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin: 50px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Card Item
|
||||
========================== */
|
||||
.featured__item {
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.25s ease,
|
||||
box-shadow 0.25s ease;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.featured__item:hover {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Image
|
||||
========================== */
|
||||
.featured__image {
|
||||
width: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Label / Title
|
||||
========================== */
|
||||
.featured__label {
|
||||
margin: 15px 0 12px 10px;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-font-dark);
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Responsive - Large
|
||||
========================== */
|
||||
@media (max-width: 1350px) {
|
||||
.featured__title {
|
||||
font-size: clamp(3.3rem, 4vw, 3.3rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.featured__title {
|
||||
font-size: clamp(3rem, 4vw, 3rem);
|
||||
}
|
||||
|
||||
.featured__label {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Responsive - Tablet
|
||||
========================== */
|
||||
@media (max-width: 900px) {
|
||||
.featured__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Responsive - Mobile
|
||||
========================== */
|
||||
@media (max-width: 810px) {
|
||||
.featured {
|
||||
padding: 50px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.featured__title {
|
||||
font-size: clamp(2rem, 4vw, 2rem);
|
||||
}
|
||||
|
||||
.featured__label {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
132
src/lib/sections/Hero.svelte
Normal file
132
src/lib/sections/Hero.svelte
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
<script>
|
||||
/* eslint-disable svelte/no-at-html-tags -- copy comes from static locale JSON */
|
||||
import { imageBaseUrl } from '$lib/config/imageCdn.js';
|
||||
import { isMobile } from '$lib/utils/isMobile';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
const backgroundImage = isMobile()
|
||||
? `${imageBaseUrl}/t/f_webp/leomurca/hero-mobile.webp`
|
||||
: `${imageBaseUrl}/t/f_webp,w_1920,h_1080/leomurca/hero.webp`;
|
||||
</script>
|
||||
|
||||
<section id="hero" style={`background-image: url(${backgroundImage});`}>
|
||||
<div class="overlay">
|
||||
<h1>{@html $t('hero.titleHtml')}</h1>
|
||||
|
||||
<p>
|
||||
{@html $t('hero.intro')}
|
||||
</p>
|
||||
|
||||
<a class="primary-button" href="#featured-works">{$t('hero.ctaWorks')}</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* =========================
|
||||
LAYOUT / STRUCTURE
|
||||
========================= */
|
||||
#hero {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
display: flex;
|
||||
background: var(--color-light-background);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
CONTENT
|
||||
========================= */
|
||||
|
||||
.overlay {
|
||||
z-index: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
max-width: 70%;
|
||||
padding-top: 200px;
|
||||
margin: 0 auto;
|
||||
|
||||
color: var(--color-font-dark);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(3.5rem, 4vw, 3.5rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
max-width: 600px;
|
||||
|
||||
margin-bottom: 2rem;
|
||||
color: #10161dcc;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
TEXT HIGHLIGHTS
|
||||
========================= */
|
||||
|
||||
/* 🔥 Strong highlight (from translated HTML) */
|
||||
.overlay :global(.highlight) {
|
||||
background: linear-gradient(120deg, #fe933540, #f1452f40);
|
||||
padding: 0 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* ✨ Softer highlight */
|
||||
.overlay :global(.highlight-soft) {
|
||||
color: #f1452f;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
MOBILE / RESPONSIVE
|
||||
========================= */
|
||||
@media (max-width: 1350px) {
|
||||
h1 {
|
||||
font-size: clamp(3.3rem, 4vw, 3.3rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
h1 {
|
||||
font-size: clamp(3rem, 4vw, 3rem);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
h1 {
|
||||
font-size: clamp(3rem, 4vw, 3rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
h1 {
|
||||
font-size: clamp(2rem, 4vw, 2rem);
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
max-width: 100%;
|
||||
padding: 100px 20px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
35
src/lib/styles/fonts.css
Normal file
35
src/lib/styles/fonts.css
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
@font-face {
|
||||
font-family: 'Bricolage Grotesque';
|
||||
font-display: swap;
|
||||
src:
|
||||
url('/fonts/bricolage.grotesque.regular.woff2') format('woff2'),
|
||||
url('/fonts/bricolage.grotesque.regular.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-optical-sizing: auto;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bricolage Grotesque';
|
||||
font-display: swap;
|
||||
src:
|
||||
url('/fonts/bricolage.grotesque.light.woff2') format('woff2'),
|
||||
url('/fonts/bricolage.grotesque.light.woff') format('woff');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-optical-sizing: auto;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bricolage Grotesque';
|
||||
font-display: swap;
|
||||
src:
|
||||
url('/fonts/bricolage.grotesque.bold.woff2') format('woff2'),
|
||||
url('/fonts/bricolage.grotesque.bold.woff') format('woff');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-optical-sizing: auto;
|
||||
}
|
||||
79
src/lib/styles/global.css
Normal file
79
src/lib/styles/global.css
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--color-light-background);
|
||||
font-family: var(--font-base);
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* region buttons */
|
||||
.primary-button {
|
||||
padding: 10px 22px;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: 0 4px 0 #cc3928;
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
.primary-button:hover {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 3px 0 #cc3928;
|
||||
}
|
||||
|
||||
.primary-button:active {
|
||||
box-shadow: 0 1px 0 #cc3928;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
padding: 10px 22px;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: 0 4px 0 #db7e2e;
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
.secondary-button:hover {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 3px 0 #db7e2e;
|
||||
}
|
||||
|
||||
.secondary-button:active {
|
||||
box-shadow: 0 1px 0 #db7e2e;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
/* endregion buttons */
|
||||
11
src/lib/styles/variables.css
Normal file
11
src/lib/styles/variables.css
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
:root {
|
||||
--color-primary: #fe9335;
|
||||
--color-secondary: #f1452f;
|
||||
--color-tertiary: #ff6900;
|
||||
--color-dark-background: #10161d;
|
||||
--color-light-background: #fff7ed;
|
||||
--color-font-dark: #242e38;
|
||||
--color-font-light: #71717c;
|
||||
|
||||
--font-base: 'Bricolage Grotesque';
|
||||
}
|
||||
10
src/lib/translations/en-US/about.json
Normal file
10
src/lib/translations/en-US/about.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"eyebrow": "About",
|
||||
"title": "Senior software engineer. Android-first. Results-focused.",
|
||||
"subtitle": "{{years}} {{yearNoun}} in software engineering, focused on production systems and accountable delivery.",
|
||||
"p1": "Active professionally since {{sinceYear}}. Work spans requirements through implementation and long-term maintenance—prioritizing predictable releases, testable behavior, and controlled technical debt.",
|
||||
"p2": "Primary technical depth on Android: Jetpack Compose, Bluetooth Low Energy (BLE), streaming pipelines, UI testing, and Kotlin Coroutines. Typical use cases include performance-sensitive apps, stable user-facing flows, and architectures that remain evolvable.",
|
||||
"p3": "Structured mentoring and onboarding for interns and early-career engineers, with emphasis on code review standards and incremental delivery. Additional hands-on web development where the product requires a web surface beyond the Android client.",
|
||||
"skills": "Jetpack Compose · BLE · Android streaming · UI testing · Coroutines · Web",
|
||||
"imageAlt": "Leonardo Murça, senior software engineer"
|
||||
}
|
||||
55
src/lib/translations/en-US/alagoas.json
Normal file
55
src/lib/translations/en-US/alagoas.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"meta": {
|
||||
"title": "SECTI Alagoas Entrepreneurship — Case Study | Leonardo Murça",
|
||||
"description": "Case study: the Empreendedorismo SECTI Alagoas landing page—promoting Lagoon Startup & VAI Startup with a fast, accessible SvelteKit build, SEO, and public-sector UX by Leonardo Murça.",
|
||||
"keywords": "SECTI Alagoas, Lagoon Startup, VAI Startup, landing page, SvelteKit, case study, Leonardo Murça, entrepreneurship, Alagoas, SEO",
|
||||
"ogTitle": "SECTI Alagoas — Startup programs landing page",
|
||||
"ogDescription": "High-performance SvelteKit landing page for state-backed startup programs: structure, performance, and conversion-focused layout.",
|
||||
"twitterDescription": "Case study: SECTI Alagoas entrepreneurship page — SvelteKit, Lagoon & VAI Startup programs."
|
||||
},
|
||||
"hero": {
|
||||
"titleHtml": "Empowering innovation <span>and startup culture</span> across Alagoas.",
|
||||
"description": "Empreendedorismo SECTI Alagoas was created to promote Lagoon Startup and VAI Startup — two public initiatives designed to support local entrepreneurs with funding, mentorship, and structured startup development programs.",
|
||||
"navAria": "Project links",
|
||||
"visitAria": "Visit Empreendedorismo SECTI Alagoas website",
|
||||
"sectiAria": "Visit SECTI Alagoas website"
|
||||
},
|
||||
"heroImgAlt": "Empreendedorismo SECTI Alagoas landing page showcase",
|
||||
"story": {
|
||||
"eyebrow": "The Project",
|
||||
"title": "Creating a digital experience to strengthen entrepreneurship in Alagoas.",
|
||||
"p1": "Empreendedorismo SECTI Alagoas was developed as a freelance project to promote Lagoon Startup and VAI Startup — two public initiatives focused on supporting local entrepreneurs through funding, mentorship, and structured startup methodologies.",
|
||||
"p2": "The challenge was not only to build a landing page, but to transform complex program information into a digital experience that felt <span class=\"highlight\">clear, accessible, and trustworthy.</span>",
|
||||
"p3": "Entrepreneurs needed an easy way to understand eligibility, registration steps, funding opportunities, and program timelines without feeling overwhelmed by information.",
|
||||
"p4": "Using SvelteKit, the platform was designed with responsiveness, accessibility, and performance as core priorities, ensuring a seamless experience across mobile, tablet, and desktop devices."
|
||||
},
|
||||
"storyImgDesktopAlt": "Empreendedorismo SECTI Alagoas homepage",
|
||||
"storyImgMobileAlt": "Empreendedorismo SECTI Alagoas responsive interface",
|
||||
"purpose": {
|
||||
"eyebrow": "Built With Purpose",
|
||||
"title": "A platform focused on clarity, accessibility, and engagement.",
|
||||
"lead": "Every section was carefully structured to guide entrepreneurs through the programs and encourage participation."
|
||||
},
|
||||
"card1": {
|
||||
"title": "Program presentation",
|
||||
"body": "Clear organization of funding details, eligibility rules, and timelines for both startup initiatives.",
|
||||
"imgAlt": "Startup programs presentation"
|
||||
},
|
||||
"card2": {
|
||||
"title": "Responsive experience",
|
||||
"body": "Optimized layouts for mobile, tablet, and desktop to ensure broad accessibility.",
|
||||
"imgAlt": "Responsive website interface"
|
||||
},
|
||||
"card3": {
|
||||
"title": "Conversion-focused structure",
|
||||
"body": "Strong calls-to-action highlighted deadlines, benefits, and next steps for entrepreneurs.",
|
||||
"imgAlt": "Call-to-action sections"
|
||||
},
|
||||
"impact": {
|
||||
"eyebrow": "Impact",
|
||||
"title": "Helping public innovation programs reach more entrepreneurs.",
|
||||
"p1": "Beyond visual design, the project focused on building credibility and visibility for entrepreneurship initiatives backed by SECTI Alagoas.",
|
||||
"p2": "Semantic HTML, SEO optimization, accessibility practices, and fast loading performance helped create a professional digital presence capable of engaging entrepreneurs across the state.",
|
||||
"p3": "The result became more than a landing page — it became a communication bridge connecting local innovators with opportunities designed to grow the startup ecosystem in Alagoas."
|
||||
}
|
||||
}
|
||||
3
src/lib/translations/en-US/brands.json
Normal file
3
src/lib/translations/en-US/brands.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "TRUSTED BY TEAMS AND BUSINESSES"
|
||||
}
|
||||
7
src/lib/translations/en-US/common.json
Normal file
7
src/lib/translations/en-US/common.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"goBack": "Go back",
|
||||
"visitWebsite": "Visit Website",
|
||||
"viewSource": "View Source",
|
||||
"aboutSecti": "About SECTI",
|
||||
"openIdentifier": "Open Identifier"
|
||||
}
|
||||
10
src/lib/translations/en-US/contact.json
Normal file
10
src/lib/translations/en-US/contact.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"mailSubject": "Hello from leomurca.xyz",
|
||||
"title": "Ready when you are",
|
||||
"lead": "If you have a product to ship, a team to strengthen, or a technical question worth a second opinion—send a message. Clear scope or rough idea: both are welcome.",
|
||||
"cta": "Start a conversation",
|
||||
"linksIntro": "Prefer a specific channel?",
|
||||
"emailLink": "leo@leomurca.xyz",
|
||||
"linkedinLink": "LinkedIn — /leonardoamurca",
|
||||
"gitLink": "git.leomurca.xyz — repos"
|
||||
}
|
||||
63
src/lib/translations/en-US/embroidery.json
Normal file
63
src/lib/translations/en-US/embroidery.json
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"meta": {
|
||||
"title": "Embroidery Viewer — Case Study | Leonardo Murça",
|
||||
"description": "Case study: Embroidery Viewer grew from a browser-based preview into a cross-platform embroidery ecosystem—SvelteKit web app, Android, performance, and UX decisions by Leonardo Murça.",
|
||||
"keywords": "Embroidery Viewer, embroidery software, embroidery preview, Android embroidery, SvelteKit, case study, Leonardo Murça, cross-platform, web app",
|
||||
"ogTitle": "Embroidery Viewer — Cross-platform case study",
|
||||
"ogDescription": "From lightweight web preview to Android and web ecosystem: architecture, rendering, and product choices behind Embroidery Viewer.",
|
||||
"twitterDescription": "Case study: Embroidery Viewer — web + Android ecosystem, SvelteKit, and craft-focused UX."
|
||||
},
|
||||
"hero": {
|
||||
"titleHtml": "Building an embroidery <span>ecosystem</span> for web and mobile.",
|
||||
"description": "What started as a lightweight browser-based embroidery preview tool evolved into a complete cross-platform experience used by embroidery enthusiasts, creators, and professionals worldwide.",
|
||||
"navAria": "Project links",
|
||||
"visitAria": "Visit Embroidery Viewer website",
|
||||
"sourceAria": "View Embroidery Viewer source code"
|
||||
},
|
||||
"heroImgAlt": "Embroidery Viewer showcase",
|
||||
"story": {
|
||||
"eyebrow": "The Journey",
|
||||
"title": "From a small browser tool to a complete embroidery ecosystem.",
|
||||
"p1": "Embroidery Viewer did not start as a business idea. It started from a simple frustration.",
|
||||
"p2": "Most embroidery preview software felt outdated, heavy, or inaccessible to casual creators. Opening a simple embroidery file often required desktop applications, complex workflows, or expensive software suites.",
|
||||
"p3": "The goal became clear: create a modern embroidery experience that felt <span class=\"highlight\">fast, accessible, and effortless.</span>",
|
||||
"p4": "The project initially began by improving an existing open-source embroidery viewer available on the web. Over time, it evolved far beyond its original implementation through multiple redesigns, feature additions, and continuous improvements focused on usability and performance."
|
||||
},
|
||||
"storyImgOldAlt": "Early Embroidery Viewer interface",
|
||||
"storyImgNewAlt": "Modern Embroidery Viewer web interface",
|
||||
"features": {
|
||||
"eyebrow": "Evolution Through Features",
|
||||
"title": "Small improvements that gradually shaped the platform.",
|
||||
"lead": "Every iteration introduced new ways to help creators inspect and validate embroidery files directly from the browser."
|
||||
},
|
||||
"feat1": {
|
||||
"title": "Thread color visualization",
|
||||
"body": "Easily inspect embroidery thread palettes and color sequences.",
|
||||
"imgAlt": "Thread colors visualization"
|
||||
},
|
||||
"feat2": {
|
||||
"title": "Stitch count insights",
|
||||
"body": "Better visibility into embroidery complexity and design details.",
|
||||
"imgAlt": "Embroidery stitch count details"
|
||||
},
|
||||
"feat3": {
|
||||
"title": "Export as image",
|
||||
"body": "Convert embroidery previews into shareable image exports directly from the platform.",
|
||||
"imgAlt": "Export embroidery design"
|
||||
},
|
||||
"mobile": {
|
||||
"eyebrow": "Android Expansion",
|
||||
"title": "Expanding the embroidery workflow beyond the browser.",
|
||||
"p1": "As the platform evolved, the next step became bringing the same experience to mobile devices.",
|
||||
"p2": "Built with Jetpack Compose, the Android application focused on touch interactions, offline support, and smoother file handling for creators working directly from phones and tablets.",
|
||||
"p3": "Today, Embroidery Viewer connects web and Android experiences into a growing cross-platform embroidery ecosystem."
|
||||
},
|
||||
"mobileImgAlt": "Embroidery Viewer Android application",
|
||||
"ai": {
|
||||
"eyebrow": "AI-Assisted Development",
|
||||
"title": "Using AI to accelerate development and creativity.",
|
||||
"p1": "AI became an important part of building the Embroidery Viewer ecosystem.",
|
||||
"p2": "It was used to help refactor code, automate repetitive tasks, generate visual assets, improve workflows, and speed up experimentation during development.",
|
||||
"p3": "Instead of replacing creativity, AI helped reduce friction and allowed more focus on product quality, usability, and iteration speed."
|
||||
}
|
||||
}
|
||||
9
src/lib/translations/en-US/featured.json
Normal file
9
src/lib/translations/en-US/featured.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"sectionTitle": "Featured Work",
|
||||
"workEmbroidery": "Embroidery Viewer",
|
||||
"workAlagoas": "Empreendedorismo Alagoas",
|
||||
"workImplant": "Implant File",
|
||||
"altEmbroidery": "Embroidery Viewer",
|
||||
"altAlagoas": "Empreendedorismo",
|
||||
"altImplant": "Implant File"
|
||||
}
|
||||
15
src/lib/translations/en-US/footer.json
Normal file
15
src/lib/translations/en-US/footer.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"logoAlt": "Leonardo Murça logo.",
|
||||
"tagline": "Engineering reliable products with simplicity, performance, and impact.",
|
||||
"contactHeading": "Get in Touch",
|
||||
"contactLead": "Have an opportunity or project? Let’s make it happen.",
|
||||
"resourcesHeading": "Resources",
|
||||
"resourcesNavAria": "Resources navigation",
|
||||
"privacyPolicy": "Privacy policy",
|
||||
"backToTop": "Back to top",
|
||||
"copyright": "All rights reserved",
|
||||
"emailAria": "Email leo@leomurca.xyz",
|
||||
"linkedinAria": "LinkedIn leonardoamurca",
|
||||
"gitRepos": "Git repositories",
|
||||
"gitAria": "Personal Git server at git.leomurca.xyz (opens in a new tab)"
|
||||
}
|
||||
11
src/lib/translations/en-US/header.json
Normal file
11
src/lib/translations/en-US/header.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"homeAria": "Home",
|
||||
"navHome": "Home",
|
||||
"navWorks": "Works",
|
||||
"navAbout": "About",
|
||||
"navContact": "Contact",
|
||||
"resume": "Resume",
|
||||
"menuAria": "Menu",
|
||||
"langToggleAria": "Language",
|
||||
"logoAlt": "Leonardo Murça"
|
||||
}
|
||||
5
src/lib/translations/en-US/hero.json
Normal file
5
src/lib/translations/en-US/hero.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"titleHtml": "I build <span class=\"highlight\">Android apps that scale</span>.<br />On the web, I create <span class=\"highlight-soft\">fast, conversion-focused products</span>.",
|
||||
"intro": "I'm <strong>Leonardo</strong>, a Senior Android Engineer focused on performance, architecture, and real-world impact — helping teams and businesses ship software that works and lasts.",
|
||||
"ctaWorks": "View my works"
|
||||
}
|
||||
54
src/lib/translations/en-US/implant-case.json
Normal file
54
src/lib/translations/en-US/implant-case.json
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"meta": {
|
||||
"title": "Implant File — Case Study | Leonardo Murça",
|
||||
"description": "Case study: redesigning Implant File into a credible healthcare web platform—AI-powered dental implant identification, multilingual SvelteKit UX, and performance by Leonardo Murça.",
|
||||
"keywords": "Implant File, dental implant, AI healthcare, SvelteKit, case study, Leonardo Murça, medical web app, implant identifier",
|
||||
"ogTitle": "Implant File — Healthcare & AI case study",
|
||||
"ogDescription": "Modern responsive UX, AI implant identification, and scalable multilingual delivery—how Implant File was rebuilt with SvelteKit.",
|
||||
"twitterDescription": "Case study: Implant File — AI dental implant ID, multilingual SvelteKit, and trustworthy medical UX."
|
||||
},
|
||||
"hero": {
|
||||
"titleHtml": "Modernizing a healthcare platform <span>with AI-powered</span> implant identification.",
|
||||
"description": "Implant File was redesigned to transform an outdated digital experience into a modern healthcare platform focused on usability, credibility, and intelligent dental implant identification powered by AI.",
|
||||
"navAria": "Project links",
|
||||
"visitAria": "Visit Implant File website",
|
||||
"identifierAria": "Visit Implant Identifier"
|
||||
},
|
||||
"heroImgAlt": "Implant File platform showcase",
|
||||
"story": {
|
||||
"eyebrow": "The Project",
|
||||
"title": "Rebuilding the digital experience around trust, usability, and AI.",
|
||||
"p1": "Implant File already had a strong product, but its website no longer reflected the same level of professionalism and quality expected from a healthcare platform.",
|
||||
"p2": "The project involved redesigning the institutional website at <span class=\"highlight\">implantfile.com.br</span> and developing a separate AI-powered implant identifier at <span class=\"highlight\">identifier.implantfile.com.br</span>.",
|
||||
"p3": "Beyond visual improvements, the goal was to create a scalable platform capable of improving SEO visibility, generating leads, and delivering a seamless cross-device experience."
|
||||
},
|
||||
"storyImgDesktopAlt": "Implant File homepage",
|
||||
"storyImgMobileAlt": "Implant File responsive interface",
|
||||
"purpose": {
|
||||
"eyebrow": "Built With Purpose",
|
||||
"title": "Combining healthcare usability with AI-powered workflows.",
|
||||
"lead": "Every part of the platform was designed to improve accessibility, navigation clarity, and professional credibility."
|
||||
},
|
||||
"card1": {
|
||||
"title": "Modern website redesign",
|
||||
"body": "A responsive and SEO-optimized healthcare platform focused on usability, performance, and multilingual support.",
|
||||
"imgAlt": "Responsive healthcare website"
|
||||
},
|
||||
"card2": {
|
||||
"title": "AI implant identifier",
|
||||
"body": "Integration with an AI-powered API capable of identifying dental implant types and manufacturers from uploaded images.",
|
||||
"imgAlt": "AI implant identifier"
|
||||
},
|
||||
"card3": {
|
||||
"title": "Trust & credibility sections",
|
||||
"body": "Carefully designed testimonials, product highlights, and credibility sections created to reinforce professionalism and strengthen user trust.",
|
||||
"imgAlt": "Testimonials and credibility sections"
|
||||
},
|
||||
"impact": {
|
||||
"eyebrow": "Impact",
|
||||
"title": "Turning a traditional website into an intelligent healthcare platform.",
|
||||
"p1": "The final result delivered a more professional digital presence capable of combining modern UX practices with AI-powered healthcare workflows.",
|
||||
"p2": "SEO improvements, responsive design, multilingual support, and AI API integration helped position Implant File as a more scalable and technology-driven platform.",
|
||||
"p3": "More than redesigning pages, the project transformed how users interact with the platform online."
|
||||
}
|
||||
}
|
||||
8
src/lib/translations/en-US/meta-home.json
Normal file
8
src/lib/translations/en-US/meta-home.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"title": "Leonardo Murça — Senior Software Engineer · Android & Web",
|
||||
"description": "Portfolio of Leonardo Murça: production Android (Kotlin, Jetpack Compose, BLE, streaming), reliable releases, and polished SvelteKit web products. Remote-friendly engineering from Brazil.",
|
||||
"keywords": "Leonardo Murça, Android engineer, Kotlin, Jetpack Compose, Bluetooth LE, SvelteKit, portfolio, software engineer, full-stack, web development",
|
||||
"ogTitle": "Leonardo Murça — Android & web engineering",
|
||||
"ogDescription": "Senior software engineer shipping Android apps and fast, maintainable web experiences. Case studies in embroidery tools, healthcare platforms, and public-sector landing pages.",
|
||||
"twitterDescription": "Senior software engineer · Kotlin, Compose & SvelteKit · Case studies and contact."
|
||||
}
|
||||
25
src/lib/translations/en-US/privacy.json
Normal file
25
src/lib/translations/en-US/privacy.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"meta": {
|
||||
"title": "Privacy policy — Leonardo Murça | leomurca.xyz",
|
||||
"description": "How leomurca.xyz handles personal data: HitKeep analytics without tracking cookies, language preference in local storage, your rights, and how to contact Leonardo Murça.",
|
||||
"keywords": "privacy policy, leomurca.xyz, Leonardo Murça, HitKeep, analytics, cookieless, GDPR transparency",
|
||||
"ogTitle": "Privacy policy — leomurca.xyz",
|
||||
"ogDescription": "Clear privacy information for leomurca.xyz: HitKeep visit analytics, no cookies for tracking, and local storage for language choice.",
|
||||
"twitterDescription": "Privacy policy for leomurca.xyz — HitKeep analytics, no tracking cookies, contact for questions."
|
||||
},
|
||||
"lastUpdated": "Last updated: May 13, 2026.",
|
||||
"title": "Privacy policy",
|
||||
"intro": "This page describes how personal data may be processed when you visit leomurca.xyz (this “site”). The goal is to be transparent and proportionate.",
|
||||
"controllerTitle": "Who is responsible",
|
||||
"controller": "The site is operated by Leonardo Murça. For privacy-related questions you can email leo@leomurca.xyz.",
|
||||
"analyticsTitle": "Visit analytics (HitKeep)",
|
||||
"analyticsHtml": "This site uses <a href=\"https://hitkeep.com/\" target=\"_blank\" rel=\"noopener noreferrer\">HitKeep</a> to measure aggregate traffic and how the site is used—for example pages viewed, general geographic region derived from network information at the time of collection, broad device category, and referring site. HitKeep is designed for privacy-conscious analytics. Data is processed to operate and improve the site; it is not used for behavioural advertising and I do not sell personal data.",
|
||||
"cookiesTitle": "Cookies",
|
||||
"cookiesHtml": "I do not use cookies on this site for analytics, advertising, or similar tracking.",
|
||||
"storageTitle": "Language preference",
|
||||
"storageHtml": "If you choose a display language on this site, that choice may be stored in your browser’s <strong>local storage</strong> so it can be applied on your next visit. This is not a cookie.",
|
||||
"rightsTitle": "Your rights",
|
||||
"rights": "Depending on where you live, you may have rights regarding access, correction, objection, or erasure of personal data. Because processing here is limited, many requests can be addressed simply by contacting me at the email above.",
|
||||
"changesTitle": "Changes",
|
||||
"changes": "I may update this policy from time to time. The “Last updated” date at the top will change when material edits are made."
|
||||
}
|
||||
199
src/lib/translations/index.js
Normal file
199
src/lib/translations/index.js
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import i18n from 'sveltekit-i18n';
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
export const SUPPORTED_LOCALES = Object.freeze({
|
||||
EN_US: 'en-US',
|
||||
PT_BR: 'pt-BR',
|
||||
});
|
||||
|
||||
const getInitialLocale = () => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const saved = localStorage.getItem('locale');
|
||||
// @ts-ignore
|
||||
if (saved && Object.values(SUPPORTED_LOCALES).includes(saved)) {
|
||||
return saved;
|
||||
}
|
||||
}
|
||||
return SUPPORTED_LOCALES.PT_BR;
|
||||
};
|
||||
|
||||
/** @type {import('sveltekit-i18n').Config<{ years?: number; sinceYear?: number; yearNoun?: string }>} */
|
||||
const config = {
|
||||
initLocale: getInitialLocale(),
|
||||
fallbackLocale: SUPPORTED_LOCALES.EN_US,
|
||||
loaders: [
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.PT_BR,
|
||||
key: 'common',
|
||||
loader: async () => (await import('./pt-BR/common.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.EN_US,
|
||||
key: 'common',
|
||||
loader: async () => (await import('./en-US/common.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.PT_BR,
|
||||
key: 'metaHome',
|
||||
routes: ['/'],
|
||||
loader: async () => (await import('./pt-BR/meta-home.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.EN_US,
|
||||
key: 'metaHome',
|
||||
routes: ['/'],
|
||||
loader: async () => (await import('./en-US/meta-home.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.PT_BR,
|
||||
key: 'header',
|
||||
loader: async () => (await import('./pt-BR/header.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.EN_US,
|
||||
key: 'header',
|
||||
loader: async () => (await import('./en-US/header.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.PT_BR,
|
||||
key: 'hero',
|
||||
routes: ['/'],
|
||||
loader: async () => (await import('./pt-BR/hero.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.EN_US,
|
||||
key: 'hero',
|
||||
routes: ['/'],
|
||||
loader: async () => (await import('./en-US/hero.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.PT_BR,
|
||||
key: 'brands',
|
||||
routes: ['/'],
|
||||
loader: async () => (await import('./pt-BR/brands.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.EN_US,
|
||||
key: 'brands',
|
||||
routes: ['/'],
|
||||
loader: async () => (await import('./en-US/brands.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.PT_BR,
|
||||
key: 'featured',
|
||||
routes: ['/'],
|
||||
loader: async () => (await import('./pt-BR/featured.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.EN_US,
|
||||
key: 'featured',
|
||||
routes: ['/'],
|
||||
loader: async () => (await import('./en-US/featured.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.PT_BR,
|
||||
key: 'about',
|
||||
routes: ['/'],
|
||||
loader: async () => (await import('./pt-BR/about.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.EN_US,
|
||||
key: 'about',
|
||||
routes: ['/'],
|
||||
loader: async () => (await import('./en-US/about.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.PT_BR,
|
||||
key: 'contact',
|
||||
routes: ['/'],
|
||||
loader: async () => (await import('./pt-BR/contact.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.EN_US,
|
||||
key: 'contact',
|
||||
routes: ['/'],
|
||||
loader: async () => (await import('./en-US/contact.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.PT_BR,
|
||||
key: 'footer',
|
||||
loader: async () => (await import('./pt-BR/footer.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.EN_US,
|
||||
key: 'footer',
|
||||
loader: async () => (await import('./en-US/footer.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.PT_BR,
|
||||
key: 'privacy',
|
||||
routes: ['/privacy-policy'],
|
||||
loader: async () => (await import('./pt-BR/privacy.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.EN_US,
|
||||
key: 'privacy',
|
||||
routes: ['/privacy-policy'],
|
||||
loader: async () => (await import('./en-US/privacy.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.PT_BR,
|
||||
key: 'embroidery',
|
||||
routes: ['/embroidery-viewer'],
|
||||
loader: async () => (await import('./pt-BR/embroidery.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.EN_US,
|
||||
key: 'embroidery',
|
||||
routes: ['/embroidery-viewer'],
|
||||
loader: async () => (await import('./en-US/embroidery.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.PT_BR,
|
||||
key: 'alagoas',
|
||||
routes: ['/entrepreneurship-alagoas'],
|
||||
loader: async () => (await import('./pt-BR/alagoas.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.EN_US,
|
||||
key: 'alagoas',
|
||||
routes: ['/entrepreneurship-alagoas'],
|
||||
loader: async () => (await import('./en-US/alagoas.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.PT_BR,
|
||||
key: 'implantCase',
|
||||
routes: ['/implant-file'],
|
||||
loader: async () => (await import('./pt-BR/implant-case.json')).default,
|
||||
},
|
||||
{
|
||||
locale: SUPPORTED_LOCALES.EN_US,
|
||||
key: 'implantCase',
|
||||
routes: ['/implant-file'],
|
||||
loader: async () => (await import('./en-US/implant-case.json')).default,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const {
|
||||
t,
|
||||
locale,
|
||||
locales,
|
||||
loading,
|
||||
loadTranslations,
|
||||
setRoute,
|
||||
setLocale,
|
||||
} = new i18n(config);
|
||||
|
||||
locale.subscribe(($locale) => {
|
||||
if (typeof localStorage !== 'undefined' && $locale) {
|
||||
const existing = localStorage.getItem('locale');
|
||||
if (existing !== $locale) {
|
||||
localStorage.setItem('locale', $locale);
|
||||
document.documentElement.lang = $locale;
|
||||
}
|
||||
}
|
||||
});
|
||||
10
src/lib/translations/pt-BR/about.json
Normal file
10
src/lib/translations/pt-BR/about.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"eyebrow": "Sobre",
|
||||
"title": "Engenheiro de software sênior. Foco em Android. Entrega mensurável.",
|
||||
"subtitle": "{{years}} {{yearNoun}} de engenharia de software, com ênfase em sistemas em produção e responsabilização pela entrega.",
|
||||
"p1": "Atuação profissional desde {{sinceYear}}. Escopo cobre requisitos, implementação e manutenção em produção—com prioridade em releases previsíveis, comportamento testável e dívida técnica controlada.",
|
||||
"p2": "Profundidade técnica principal em Android: Jetpack Compose, Bluetooth Low Energy (BLE), pipelines de streaming, testes de UI e Coroutines em Kotlin. Casos recorrentes incluem apps sensíveis a performance, fluxos estáveis para o usuário final e arquiteturas que permanecem evolutivas.",
|
||||
"p3": "Mentoria e onboarding estruturados para estagiários e engenheiros em início de carreira, com ênfase em padrões de revisão de código e entrega incremental. Experiência prática em desenvolvimento web quando o produto exige uma camada além do cliente Android.",
|
||||
"skills": "Jetpack Compose · BLE · Streaming Android · Testes de UI · Coroutines · Web",
|
||||
"imageAlt": "Leonardo Murça, engenheiro de software sênior"
|
||||
}
|
||||
55
src/lib/translations/pt-BR/alagoas.json
Normal file
55
src/lib/translations/pt-BR/alagoas.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"meta": {
|
||||
"title": "Empreendedorismo SECTI Alagoas — Estudo de caso | Leonardo Murça",
|
||||
"description": "Estudo de caso: landing Empreendedorismo SECTI Alagoas para Lagoon Startup e VAI Startup—SvelteKit rápido, acessível, SEO e UX para o setor público, por Leonardo Murça.",
|
||||
"keywords": "SECTI Alagoas, Lagoon Startup, VAI Startup, landing page, SvelteKit, estudo de caso, Leonardo Murça, empreendedorismo, Alagoas, SEO",
|
||||
"ogTitle": "SECTI Alagoas — Landing de programas de startups",
|
||||
"ogDescription": "Landing em SvelteKit para iniciativas públicas de startups: estrutura, performance e layout focado em conversão.",
|
||||
"twitterDescription": "Estudo de caso: página de empreendedorismo SECTI Alagoas — SvelteKit, Lagoon e VAI Startup."
|
||||
},
|
||||
"hero": {
|
||||
"titleHtml": "Fortalecendo inovação <span>e cultura de startups</span> em Alagoas.",
|
||||
"description": "Empreendedorismo SECTI Alagoas foi criado para divulgar Lagoon Startup e VAI Startup — duas iniciativas públicas com apoio a empreendedores locais por meio de recursos, mentoria e programas estruturados de desenvolvimento de startups.",
|
||||
"navAria": "Links do projeto",
|
||||
"visitAria": "Visitar site Empreendedorismo SECTI Alagoas",
|
||||
"sectiAria": "Visitar site da SECTI Alagoas"
|
||||
},
|
||||
"heroImgAlt": "Destaque da landing page Empreendedorismo SECTI Alagoas",
|
||||
"story": {
|
||||
"eyebrow": "O projeto",
|
||||
"title": "Criando uma experiência digital para fortalecer o empreendedorismo em Alagoas.",
|
||||
"p1": "Empreendedorismo SECTI Alagoas foi desenvolvido como projeto freelance para promover Lagoon Startup e VAI Startup — iniciativas públicas com foco em apoio a empreendedores por meio de recursos, mentoria e metodologias estruturadas.",
|
||||
"p2": "O desafio não era só construir uma landing page, mas transformar informações complexas em uma experiência <span class=\"highlight\">clara, acessível e confiável.</span>",
|
||||
"p3": "Empreendedores precisavam entender elegibilidade, etapas de inscrição, oportunidades de fomento e cronogramas sem se sentir sobrecarregados.",
|
||||
"p4": "Com SvelteKit, a plataforma priorizou responsividade, acessibilidade e performance para uma experiência fluida em mobile, tablet e desktop."
|
||||
},
|
||||
"storyImgDesktopAlt": "Página inicial Empreendedorismo SECTI Alagoas",
|
||||
"storyImgMobileAlt": "Interface responsiva Empreendedorismo SECTI Alagoas",
|
||||
"purpose": {
|
||||
"eyebrow": "Feito com propósito",
|
||||
"title": "Uma plataforma focada em clareza, acessibilidade e engajamento.",
|
||||
"lead": "Cada seção foi estruturada para guiar empreendedores pelos programas e incentivar a participação."
|
||||
},
|
||||
"card1": {
|
||||
"title": "Apresentação dos programas",
|
||||
"body": "Organização clara de regras de elegibilidade, fomento e cronogramas das duas iniciativas.",
|
||||
"imgAlt": "Apresentação dos programas de startups"
|
||||
},
|
||||
"card2": {
|
||||
"title": "Experiência responsiva",
|
||||
"body": "Layouts otimizados para ampliar o acesso em mobile, tablet e desktop.",
|
||||
"imgAlt": "Interface responsiva do site"
|
||||
},
|
||||
"card3": {
|
||||
"title": "Estrutura focada em conversão",
|
||||
"body": "CTAs destacando prazos, benefícios e próximos passos para os empreendedores.",
|
||||
"imgAlt": "Seções de chamada para ação"
|
||||
},
|
||||
"impact": {
|
||||
"eyebrow": "Impacto",
|
||||
"title": "Ajudando programas públicos de inovação a alcançar mais empreendedores.",
|
||||
"p1": "Além do visual, o projeto reforçou credibilidade e visibilidade das iniciativas da SECTI Alagoas.",
|
||||
"p2": "HTML semântico, SEO, acessibilidade e performance rápida ajudaram a criar uma presença digital profissional para engajar empreendedores em todo o estado.",
|
||||
"p3": "O resultado foi mais que uma landing page — virou uma ponte de comunicação entre inovadores locais e oportunidades para o ecossistema de startups em Alagoas."
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue