Compare commits

..

22 Commits

Author SHA1 Message Date
a8a64031bb Removing post
All checks were successful
Build and Deploy docker container / build (push) Successful in 5m16s
2025-04-15 22:17:11 -05:00
58b12534bb New blog post
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m19s
2025-02-07 00:15:58 -06:00
e1a3d59894 Adjusted umami host url
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m34s
2025-01-30 12:22:37 -06:00
6ba9b29daf changed the csp
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m6s
2025-01-28 18:21:52 -06:00
a4195f1ee5 updated linting issue
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m5s
2025-01-28 17:58:32 -06:00
ee886d4d13 updated umami
Some checks failed
Build and Deploy docker container / build (push) Has been cancelled
2025-01-28 17:57:51 -06:00
bbc1567ea4 updated headers to capitilize the letters
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m0s
2025-01-28 14:34:28 -06:00
418de925c4 added resume to navlinks
Some checks failed
Build and Deploy docker container / build (push) Has been cancelled
2025-01-28 14:32:15 -06:00
676b0c4ac1 hide nav link
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m9s
2025-01-28 04:47:12 -06:00
4bf423525a updated header and tagline
All checks were successful
Build and Deploy docker container / build (push) Successful in 5m4s
2025-01-28 04:41:15 -06:00
6dcfad7f0d qbitmaid
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m54s
2025-01-28 00:22:40 -06:00
78ac5b0423 publish qbitmaid 2025-01-28 00:22:19 -06:00
79b1b2db40 updated build.yml
All checks were successful
Build and Deploy docker container / build (push) Successful in 2m52s
2025-01-25 16:16:05 -06:00
30fdb4a3b9 test
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m31s
2025-01-25 12:16:06 -06:00
292439fe68 test act runner 2025-01-25 12:09:18 -06:00
fafc6ed859 test build.yml 2025-01-25 12:04:25 -06:00
a79eac496a Merge pull request 'updated build.yml' (#2) from update-readme into main
Reviewed-on: #2
2025-01-25 12:01:04 -06:00
e06089e6a6 updated build.yml 2025-01-25 12:00:29 -06:00
1debc3e50c Changed twitter card 2025-01-25 11:38:28 -06:00
34d1ec5006 Added react-doc-viewer 2025-01-25 11:28:33 -06:00
a1a3e21266 merging latest changes 2025-01-25 10:57:53 -06:00
6bfe06f65c Updated site icons and added resume to about 2025-01-25 10:57:16 -06:00
45 changed files with 19183 additions and 1949 deletions

View File

@ -1,2 +0,0 @@
node_modules
.eslintrc.js

View File

@ -1,42 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
env: {
browser: true,
amd: true,
node: true,
es6: true,
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jsx-a11y/recommended',
'plugin:prettier/recommended',
'next',
'next/core-web-vitals',
],
parserOptions: {
project: true,
tsconfigRootDir: __dirname,
},
rules: {
'prettier/prettier': 'error',
'react/react-in-jsx-scope': 'off',
'jsx-a11y/anchor-is-valid': [
'error',
{
components: ['Link'],
specialLink: ['hrefLeft', 'hrefRight'],
aspects: ['invalidHref', 'preferButton'],
},
],
'react/prop-types': 0,
'@typescript-eslint/no-unused-vars': 0,
'react/no-unescaped-entities': 0,
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
},
}

View File

@ -1,7 +1,10 @@
name: Build and Deploy docker container
'on':
on:
push:
branches: main
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
@ -56,7 +59,7 @@ jobs:
- name: Deploy app
uses: fjogeleit/http-request-action@v1
with:
url: 'http://192.168.4.11:7777/v1/update'
url: 'http://10.5.0.11:7777/v1/update'
method: GET
bearerToken: '${{ secrets.DEPLOYTOKEN }}'
timeout: 60000

View File

@ -1,6 +1,8 @@
![tailwind-nextjs-banner](/public/static/images/twitter-card.png)
# Tailwind Nextjs Starter Blog
# Joyful Fixations
> This template has been adapted for my needs. Please view the upstream if you wish to create a similar site.
[![GitHub Repo stars](https://img.shields.io/github/stars/timlrx/tailwind-nextjs-starter-blog?style=social)](https://GitHub.com/timlrx/tailwind-nextjs-starter-blog/stargazers/)
[![GitHub forks](https://img.shields.io/github/forks/timlrx/tailwind-nextjs-starter-blog?style=social)](https://GitHub.com/timlrx/tailwind-nextjs-starter-blog/network/)

View File

@ -67,29 +67,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
className={`${space_grotesk.variable} scroll-smooth`}
suppressHydrationWarning
>
<link
rel="apple-touch-icon"
sizes="76x76"
href={`${basePath}/static/favicons/apple-touch-icon.png`}
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href={`${basePath}/static/favicons/favicon-32x32.png`}
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href={`${basePath}/static/favicons/favicon-16x16.png`}
/>
<link rel="manifest" href={`${basePath}/static/favicons/site.webmanifest`} />
<link
rel="mask-icon"
href={`${basePath}/static/favicons/safari-pinned-tab.svg`}
color="#5bbad5"
/>
<link rel="icon" type="image/png" href="/static/favicons/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/static/favicons/favicon.svg" />
<link rel="shortcut icon" href="/static/favicons/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicons/apple-touch-icon.png" />
<link rel="manifest" href="/static/favicons/site.webmanifest" />
<meta name="msapplication-TileColor" content="#000000" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />

View File

@ -1 +1 @@
{"art":1,"store":1,"melis-sweetsimple":1,"visual-studio-code":1,"guide":4,"tailwind-nextjs-starter-blog":1,"disney":1,"github":1,"python":1,"projects":1,"code":1,"cygnus":1,"self-hosted":1,"server":1,"next-js":1}
{"tag1":1,"tag2":1,"tag3":1,"art":1,"store":1,"melis-sweetsimple":1,"visual-studio-code":1,"guide":4,"tailwind-nextjs-starter-blog":1,"disney":1,"github":1,"python":1,"projects":1,"code":1,"docker":1,"unraid":1,"cygnus":1,"self-hosted":1,"server":1,"next-js":1}

View File

@ -17,6 +17,7 @@ export default function Footer() {
<SocialIcon kind="instagram" href={siteMetadata.instagram} size={6} />
<SocialIcon kind="threads" href={siteMetadata.threads} size={6} />
<SocialIcon kind="medium" href={siteMetadata.medium} size={6} />
<SocialIcon kind="resume" href={siteMetadata.resume} size={6} />
</div>
<div className="mb-2 flex space-x-2 text-sm text-gray-500 dark:text-gray-400">
<div>{siteMetadata.author}</div>
@ -26,7 +27,7 @@ export default function Footer() {
<Link href="/">{siteMetadata.title}</Link>
</div>
<div className="mb-8 text-sm text-gray-500 dark:text-gray-400">
<Link href="https://github.com/timlrx/tailwind-nextjs-starter-blog">Theme</Link>
<Link href="https://git.jonb.io/jblu/jonbio">Theme</Link>
</div>
</div>
</footer>

View File

@ -29,7 +29,7 @@ const Header = () => {
</div>
</Link>
<div className="flex items-center space-x-4 leading-5 sm:space-x-6">
<div className="no-scrollbar hidden max-w-40 items-center space-x-4 overflow-x-auto sm:flex sm:space-x-6 md:max-w-72 lg:max-w-96">
<div className="no-scrollbar hidden max-w-40 items-center space-x-4 overflow-x-scroll sm:flex sm:space-x-3 md:max-w-72 lg:max-w-96">
{headerNavLinks
.filter((link) => link.href !== '/')
.map((link) => (

View File

@ -102,3 +102,12 @@ export function Medium(svgProps: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function Resume(svgProps: SVGProps<SVGSVGElement>) {
return (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...svgProps}>
<title>Resume</title>
<path d="M14.727 6.727H14V0H4.91c-.905 0-1.637.732-1.637 1.636v20.728c0 .904.732 1.636 1.636 1.636h14.182c.904 0 1.636-.732 1.636-1.636V6.727h-6zm-.545 10.455H7.09v-1.364h7.09v1.364zm2.727-3.273H7.091v-1.364h9.818v1.364zm0-3.273H7.091V9.273h9.818v1.363zM14.727 6h6l-6-6v6z" />
</svg>
)
}

View File

@ -10,6 +10,7 @@ import {
Threads,
Instagram,
Medium,
Resume,
} from './icons'
const components = {
@ -24,6 +25,7 @@ const components = {
threads: Threads,
instagram: Instagram,
medium: Medium,
resume: Resume,
}
type SocialIconProps = {

View File

@ -140,6 +140,7 @@ export const Authors = defineDocumentType(() => ({
linkedin: { type: 'string' },
github: { type: 'string' },
layout: { type: 'string' },
resume: { type: 'string' },
},
computedFields,
}))

View File

@ -2,15 +2,16 @@
name: Jonathan Branan
avatar: https://s3.jonb.io/cdn/author/2.JPG
occupation: Software Engineer
company: Fortra
email: jonathan.branan@fortra.com
# company:
email: jonbranan@gmail.com
linkedin: https://www.linkedin.com/in/jonathanbranan/
github: https://github.com/jonbranan
resume: https://s3.jonb.io/cdn/author/Resume.pdf
---
Jonathan Branan is a Software Engineer at Fortra. He is mostly self-taught however he did attend a vocational class at Rackspace Open Cloud Academy.
Jonathan Branan is a Software Engineer. He is mostly self-taught however he did attend a vocational class at Rackspace Open Cloud Academy.
He has worked for Geek Squad repairing computers, GlobalScape as a Lead of the Client Services department and as a Product Owner of MFT applications at Fortra.
He has worked for Geek Squad repairing computers, GlobalScape as a Lead of the Client Services department, a Product Owner of MFT applications and as a Software Engineer at Fortra.
He currently lives in San Antonio, Texas with his wife and two dogs. He enjoys Basketball, Video games, cooking, camping(backpacking, glamping), watching movies,
making mixed drinks and building Lego's. Jonathan and his wife like to frequently travel.

10
data/blog/blogideas.mdx Normal file
View File

@ -0,0 +1,10 @@
---
title: 'idea'
date: '2025-1-1'
#lastmod: '202year-month-day'
draft: true
---
- split dns jonb.io vs int.jonb.io
- entertainment stack
<video controls autoPlay playsInline src="https://youtu.be/M1DJSXgJrDc?si=Rasg9u8muFno_si8"/>

View File

@ -1,16 +1,15 @@
---
title: 'Using Visual Studio Code to quickly write frontmatter'
date: '2024-11-12'
#lastmod: '202year-day-month'
tags: ['Visual Studio Code', 'guide', 'Tailwind Nextjs Starter Blog']
draft: true
date: '2024-2-7'
tags: ['Visual Studio Code', 'guide', 'code']
draft: false
summary: 'summary'
#images: ['/static/images/image.jpg',]
#authors: ['default',]
#layout: PostLayout
#canonicalUrl: https://jonb.io/blog/the-url-here
---
1. Paste the following into your md or mdx snippet file.In Visual Studio Code, go to File --> Preferences --> Configure Snippets.
2. To use a snippet, type the prefix and you will get snippet suggestions.
```json
{
"Blog Frontmatter": {

View File

@ -1,30 +0,0 @@
---
title: 'Introducing Melis Sweet&Simple'
date: '2024-11-14'
#lastmod: '202year-month-day'
tags: ['Art', 'store', 'Melis Sweet&Simple']
draft: false
summary: 'Elevate your Space with Bespoke Handcrafted Art from our Store that is Super Sweet and Simple.'
images: ['','','']
authors: ['default','meli']
layout: PostLayout # PostLayout, PostSimple and PostBanner
#canonicalUrl: https://jonb.io/blog/the-url-here
---
## Meli's Sweet&Simple
> Elevate your Space with Bespoke Handcrafted Art from our Store that is Super Sweet and Simple.
[Check it out!](https://melisweetsimple.jonb.io/)
<div className="-mx-2 flex flex-wrap overflow-hidden xl:-mx-2">
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
![walle](https://melisweetsimple.jonb.io/cdn/shop/files/FullSizeRender.heic?v=1730088140&width=2200)
</div>
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
![walleclose](https://melisweetsimple.jonb.io/cdn/shop/files/FullSizeRender_4297464f-1fe2-4809-9694-72058873bed8.heic?v=1730088140&width=2200)
</div>
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
![bird](https://melisweetsimple.jonb.io/cdn/shop/files/A_Whirlwind_6713f0cd-3ed1-4e75-a825-504ad1c88e66.png?v=1729026172&width=2048)
</div>
</div>

View File

@ -1,22 +1,24 @@
---
title: Qbitmaid
date: '2024-11-3'
lastmod: '2024-11-14'
tags: ['python', 'projects', 'code']
draft: true
lastmod: '2025-1-28'
tags: ['python', 'projects', 'code', 'docker', 'unraid']
draft: false
summary: How I used python to keep my torrents in check
layout: PostBanner
images: ['https://s3.jonb.io/cdn/projects/qbitmaid.jpg']
---
### qbit-maid
Development [^1] of qbitmaid was over the course of several months. At first, the project was called qbit-clean and didn't have all the features the project has now. The issue was mainly with my download cache in unraid being filled with torrents I no longer needed to seed[^2]. When I would get a notification from the server that the download cache was 95% full I would have to manually go to [qbittorrent](https://www.qbittorrent.org/), sort the torrents by age and remove the ones older than two weeks avoiding torrents I wanted to keep.
![qbittorrent](https://www.qbittorrent.org/img/qb_banner.webp)
[![qbittorrent](https://www.qbittorrent.org/img/qb_banner.webp)](https://www.qbittorrent.org/)
This was tedious. Very tedious. So I went off to do more work just to avoid a little.
This was tedious. Very tedious. So I went off to do more work just to avoid a little.`qbitmaid.py` is the main file that glues the project together. This was my first project where I heavily abstracted the design. More on this later. First, we'll connect to the API. I used an existing client [library](https://pypi.org/project/qbittorrent-api/2022.5.32/) that makes this process simpler than writing your own client.
`qbitmaid.py`[^3]
```python
...
class Qbt:
@ -31,38 +33,157 @@ class Qbt:
except qbittorrentapi.APIError as e:
self.tl.exception(e)
self.po.send_message(e, title="qbit-maid API ERROR")
# Pulling all torrent data
self.torrent_list = self.qbt_client.torrents_info()
#Main process block
if self.use_log:
list_qbit_api_info(self)
list_first_tor(self)
debug_torrent_list(self)
build_tor_list(self)
process_counts(self)
if self.use_log:
torrent_count(self)
tor_processor(self)
if self.use_log:
print_processor(self)
if self.delete_torrents:
tor_delete(self)
self.et = datetime.datetime.now()
get_script_runtime(self)
if self.use_pushover:
tor_notify_summary(self)
if self.use_apprise:
tor_notify_apprise(self, r, apprise_notify)
if self.use_healthcheck:
send_ping(self, r, self.healthcheck_url)
# Run
if __name__== "__main__":
Qbt()
...
```
This is the main file that glues the project together. This was my first project where I heavily
Then we use the api to make an list of the torrents:
```python
# Pulling all torrent data
self.torrent_list = self.qbt_client.torrents_info()
```
Next, we "sift" out torrents to be deleted. This was created with a positive sieve meaning we specify positive scenarios. In other words, I know which torrents I want to keep as opposed to the torrents I don't want. Theres pros and cons to both scenarios however in the long term a positive sieve is less work.
> `qlist.py` has functions at the bottom of the file that are referenced in the conditions. This very method of programming made it easy to write unit tests as I went.
```python
if is_preme(torrent['seeding_time'], self.min_age):
continue
```
```python
def is_preme(seeding_time, minage):
if seeding_time <= minage:
return True
```
When it comes across an item that meets certain criteria it will skip it. For instance, the example above checks to see if it's too soon to remove a torrent. This is because some [trackers][private tracker] require a minimum seed time. If you were to remove a torrent sooner than they require, it could lead to getting kicked.
`qlist.py` Has a couple jobs:
- Tag torrents according to how they should be treated.
- Sort
`qlist.py`
```python
...
def build_tor_list(self):
while self.torrent_list:
...
if is_tracker_blank(torrent['tracker']):
...
continue
elif is_cat_ignored(torrent['category'], self.cat_whitelist.values()):
...
continue
elif is_ignored_tag(self.ignored_tags.values(),torrent['tags']):
...
continue
if is_tag_blank(torrent['tags']):
...
if is_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()):
self.qbt_client.torrents_add_tags(self.tracker_protected_tag,torrent['hash'])
elif is_not_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()):
self.qbt_client.torrents_add_tags(self.tracker_non_protected_tag,torrent['hash'])
if is_preme(torrent['seeding_time'], self.min_age):
continue
elif is_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()):
if is_tag_blank(torrent['tags']):
self.qbt_client.torrents_add_tags(self.tracker_protected_tag,torrent['hash'])
...
self.tracker_list.append(torrent)
elif is_not_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()):
if is_tag_blank(torrent['tags']):
self.qbt_client.torrents_add_tags(self.tracker_non_protected_tag,torrent['hash'])
...
self.tracker_list.append(torrent)
```
In this case the items I want to keep stays. `qlist.py` passes the data over to `qprocess.py`. This is done through about 2 layers of abstraction. Unfortunatley, this type of programming makes it difficult to follow.
`qprocess.py` has four jobs:
- Collect telemetry
- Delete torrents if needed
- Enable debugging if needed
`qprocess.py`
```python
def tor_processor(self):
"""Main logic to sort through both self.tracker_nonprotected_list and self.tracker_protected_list
If torrent meets criteria for deletion, its infohash_v1 will be appended to self.torrent_hash_delete_list
"""
for canidate in self.tracker_list:
if self.enable_telemetry:
header = ['state','ratio','tags','added','hash','name','tracker']
row = [canidate['state'],canidate['ratio'],canidate["tags"],canidate['added_on'],canidate['infohash_v1'],canidate["name"][0:20],canidate['tracker']]
write_csv(self.cv,self.telemetry_outfile,header,row)
...
elif is_protected_over_ratio(canidate['ratio'], 1.05, self.tracker_protected_tag, canidate["tags"]):
if self.use_log:
self.tl.debug(f'["{canidate["name"][0:20]}..."] is above a 1.05 ratio({canidate["ratio"]}).')
self.torrent_hash_delete_list.append(canidate['infohash_v1'])
...
elif is_not_protected_tor(self.tracker_non_protected_tag, canidate["tags"]):
self.torrent_hash_delete_list.append(canidate['infohash_v1'])
...
else:
if self.enable_dragnet:
header = ['state','ratio','tags','added','thash','tname','trname']
row = [canidate['state'],canidate['ratio'],canidate["tags"],canidate['added_on'],canidate['infohash_v1'],canidate["name"][0:20],canidate['tracker']]
write_csv(self.cv,self.dragnet_outfile,header,row)
continue
```
I package this in a docker file:
```Dockerfile
FROM python:alpine3.18
WORKDIR /
COPY . opt
RUN apk add --no-cache supercronic
RUN pip install requests
RUN pip install qbittorrent-api
RUN chmod +x /opt/entrypoint.sh
CMD ["/opt/entrypoint.sh"]
```
Then use [Drone][drone] to package this into a container. This pushes the container to an [OCI repo][oci] in gitea. The application is configured through a toml file:
```toml
[qbittorrent]
host = "192.168.x.x"
port = 8080
username = "user"
password = "pass"
...
[healthcheck]
use_healthcheck = true
healthcheck_url = "https://example.com/ping/<uuid>>"
```
Using unraid has honestly been a delight. I had some performance issues but that was due to how I was using the storage pool.
![unraid](https://s3.jonb.io/cdn/blog/qbitmaid/unraid.png)
Finally, this same container will run the test cases in `test_qbitmaid.py`. This is handled by drone. So eachtime I push new code to a development branch on gitea, it creates a container to test and tests the code. Once I see that it has passed, I can merge the code to the main branch.
## Final Notes
I have been using this for over 2 years. It was a huge learning experience and my coding practices have evolved over my newer projects. While I did make this for my use mainly, feel free to try it out! If you have any questions, you can open an issue [here](https://git.jonb.io/jblu/qbit-maid/issues/new).
[^1]: The Source Code can be found [here][source-code].
[^2]: Private trackers require you to seed a torrent for a period of time. In my case, I have to seed for about 2 weeks or to a ratio of 1.
[^3]: Code has been removed for examples in this article.
[source-code]: https://git.jonb.io/jblu/qbit-maid
[private tracker]: https://en.wikipedia.org/wiki/BitTorrent_tracker#Private_trackers
[drone]: https://www.drone.io/
[oci]: https://git.jonb.io/jblu/-/packages/container/qbit-maid/latest

View File

@ -1,10 +1,10 @@
const headerNavLinks = [
{ href: '/', title: 'Home' },
{ href: '/blog', title: 'Blog' },
// { href: 'https://git.jonb.io/', title: 'Code' },
{ href: '/likes', title: 'Likes' },
{ href: '/tags', title: 'Tags' },
{ href: '/projects', title: 'Projects' },
{ href: 'https://s3.jonb.io/cdn/author/Resume.pdf', title: 'Resume' },
{ href: '/about', title: 'About' },
]

3
data/logo copy.svg Normal file
View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="344.5639097744361 330.27819548872174 111.73684210526318 91.21804511278197" width="53.87" height="43.61"><defs><path d="M453.3 331.28L453.3 359.85L388.64 418.5L388.64 388.42L453.3 331.28Z" id="aFZf6T5ED"></path><linearGradient id="gradientb2ThqnP5Op" gradientUnits="userSpaceOnUse" x1="420.97" y1="331.28" x2="420.97" y2="418.5"><stop style="stop-color: #06b6d4;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #67e8f9;stop-opacity: 1" offset="100%"></stop></linearGradient><path d="M410.23 331.28L410.23 359.85L345.56 418.5L345.56 388.42L410.23 331.28Z" id="a9fehgwfM"></path><linearGradient id="gradientk1wNV9Ostb" gradientUnits="userSpaceOnUse" x1="377.89" y1="331.28" x2="377.89" y2="418.5"><stop style="stop-color: #06b6d4;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #67e8f9;stop-opacity: 1" offset="100%"></stop></linearGradient></defs><g><g><use xlink:href="#aFZf6T5ED" opacity="1" fill="url(#gradientb2ThqnP5Op)"></use></g><g><use xlink:href="#a9fehgwfM" opacity="1" fill="url(#gradientk1wNV9Ostb)"></use></g></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -3,7 +3,7 @@ const siteMetadata = {
title: 'Joyful Fixations',
author: 'Jonathan Branan',
headerTitle: 'Joyful Fixations',
description: "Thought's from someone with an internet connection.",
description: 'Things that bring me joy and intense focus.',
language: 'en-us',
theme: 'system', // system, dark or light
siteUrl: 'https://jonb.io',
@ -21,6 +21,7 @@ const siteMetadata = {
// threads: 'https://www.threads.net',
// instagram: 'https://www.instagram.com',
// medium: 'https://medium.com',
resume: 'https://s3.jonb.io/cdn/author/Resume.pdf',
locale: 'en-US',
// set to true if you want a navbar fixed to the top
stickyNav: true,
@ -32,7 +33,8 @@ const siteMetadata = {
// We use an env variable for this site to avoid other users cloning our analytics ID
umamiWebsiteId: process.env.NEXT_UMAMI_ID, // e.g. 123e4567-e89b-12d3-a456-426614174000
// You may also need to overwrite the script if you're storing data in the US - ex:
// src: 'https://us.umami.is/script.js'
src: 'https://umami.jonb.io/script.js',
umamiHostUrl: 'https://umami.jonb.io',
// Remember to add 'us.umami.is' in `next.config.js` as a permitted domain for the CSP
},
// plausibleAnalytics: {

70
eslint.config.mjs Normal file
View File

@ -0,0 +1,70 @@
import typescriptEslint from '@typescript-eslint/eslint-plugin'
import globals from 'globals'
import tsParser from '@typescript-eslint/parser'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import js from '@eslint/js'
import { FlatCompat } from '@eslint/eslintrc'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
})
export default [
{
ignores: [],
},
js.configs.recommended,
...compat.extends(
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jsx-a11y/recommended',
'plugin:prettier/recommended',
'next',
'next/core-web-vitals'
),
{
plugins: {
'@typescript-eslint': typescriptEslint,
},
languageOptions: {
globals: {
...globals.browser,
...globals.amd,
...globals.node,
},
parser: tsParser,
ecmaVersion: 5,
sourceType: 'commonjs',
parserOptions: {
project: true,
tsconfigRootDir: __dirname,
},
},
rules: {
'prettier/prettier': 'error',
'react/react-in-jsx-scope': 'off',
'jsx-a11y/anchor-is-valid': [
'error',
{
components: ['Link'],
specialLink: ['hrefLeft', 'hrefRight'],
aspects: ['invalidHref', 'preferButton'],
},
],
'react/prop-types': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'react/no-unescaped-entities': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
},
},
]

View File

@ -9,7 +9,7 @@ interface Props {
}
export default function AuthorLayout({ children, content }: Props) {
const { name, avatar, occupation, company, email, twitter, linkedin, github } = content
const { name, avatar, occupation, company, email, twitter, linkedin, github, resume } = content
return (
<>
@ -38,6 +38,7 @@ export default function AuthorLayout({ children, content }: Props) {
<SocialIcon kind="github" href={github} />
<SocialIcon kind="linkedin" href={linkedin} />
<SocialIcon kind="x" href={twitter} />
<SocialIcon kind="resume" href={resume} />
</div>
</div>
<div className="prose max-w-none pb-8 pt-8 dark:prose-invert xl:col-span-2">

View File

@ -1,4 +1,3 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
'use client'
import { usePathname } from 'next/navigation'

2
next-env.d.ts vendored
View File

@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -7,7 +7,7 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
// You might need to insert additional domains in script-src if you are using external services
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline' giscus.app analytics.umami.is;
script-src 'self' 'unsafe-eval' 'unsafe-inline' giscus.app umami.jonb.io;
style-src 'self' 'unsafe-inline';
img-src * blob: data:;
media-src s3.jonb.io* melisweetsimple.jonb.io*;

16561
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
},
"dependencies": {
"@headlessui/react": "2.2.0",
"@next/bundle-analyzer": "15.0.2",
"@next/bundle-analyzer": "15.1.4",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"autoprefixer": "^10.4.13",
@ -24,12 +24,13 @@
"gray-matter": "^4.0.2",
"hast-util-from-html-isomorphic": "^2.0.0",
"image-size": "1.0.0",
"next": "15.0.2",
"next": "15.1.4",
"next-contentlayer2": "0.5.3",
"next-themes": "^0.3.0",
"pliny": "0.4.0",
"postcss": "^8.4.24",
"react": "rc",
"react-doc-viewer": "^0.1.14",
"react-dom": "rc",
"reading-time": "1.5.0",
"rehype-autolink-headings": "^7.1.0",
@ -47,6 +48,8 @@
"unist-util-visit": "^5.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.16.0",
"@svgr/webpack": "^8.0.1",
"@types/mdx": "^2.0.12",
"@types/react": "^18.2.73",
@ -54,9 +57,10 @@
"@typescript-eslint/parser": "^8.12.0",
"cross-env": "^7.0.3",
"eslint": "^9.14.0",
"eslint-config-next": "15.0.2",
"eslint-config-next": "15.1.4",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.0",
"globals": "^15.12.0",
"husky": "^9.0.0",
"lint-staged": "^13.0.0",
"prettier": "^3.0.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#000000</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -1,21 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="107.000000pt" height="107.000000pt" viewBox="0 0 107.000000 107.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,107.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M609 942 c-17 -18 -95 -88 -289 -258 -117 -102 -177 -155 -204 -179
-6 -5 -31 -28 -56 -50 l-45 -39 -1 -144 -1 -143 21 18 c11 10 25 23 31 28 7 6
46 41 88 79 86 77 86 77 216 196 52 47 103 93 115 103 11 10 50 45 86 77 l65
59 -1 131 c0 71 -3 132 -5 135 -3 2 -12 -3 -20 -13z"/>
<path d="M929 857 c-64 -56 -145 -128 -180 -159 -35 -31 -75 -66 -89 -78 -55
-47 -68 -59 -150 -131 l-85 -76 2 -129 c1 -71 2 -135 2 -143 1 -11 5 -10 19 5
10 10 54 51 98 89 43 39 92 83 109 99 26 24 63 58 189 171 12 11 47 43 77 70
30 28 70 63 89 79 l35 29 1 138 c1 77 1 139 0 139 0 -1 -53 -47 -117 -103z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,14 +1,21 @@
{
"name": "",
"short_name": "",
"name": "Joyful Fixations",
"short_name": "Joyful Fixations",
"icons": [
{
"src": "/android-chrome-96x96.png",
"sizes": "96x96",
"type": "image/png"
"src": "/static/favicons/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/favicons/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#000000",
"background_color": "#000000",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

4041
yarn.lock

File diff suppressed because it is too large Load Diff