feat: move to next.js (!14)

Reviewed-on: https://codeberg.org/nhcarrigan/portfolio/pulls/14
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit is contained in:
Naomi Carrigan 2024-08-25 02:40:46 +00:00 committed by Naomi the Technomancer
parent 6b7c8b2256
commit e22a51ba23
146 changed files with 3582 additions and 11995 deletions

View File

@ -1,16 +0,0 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View File

@ -1,3 +1,3 @@
{ {
"extends": "@nhcarrigan/eslint-config-angular" "extends": "next/core-web-vitals"
} }

66
.gitignore vendored
View File

@ -1,42 +1,36 @@
# See http://help.github.com/ignore-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Compiled output # dependencies
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules /node_modules
npm-debug.log /.pnp
yarn-error.log .pnp.js
.yarn/install-state.gz
# IDEs and editors # testing
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage /coverage
/libpeerconnection.log
testem.log
/typings
# System files # next.js
/.next/
/out/
# production
/build
# misc
.DS_Store .DS_Store
Thumbs.db *.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -1 +0,0 @@
"@nhcarrigan/prettier-config"

View File

@ -1,3 +0,0 @@
# Code of Conduct
Our Code of Conduct can be found here: https://docs.nhcarrigan.com/#/coc

View File

@ -1,3 +0,0 @@
# Contributing
Our contributing guidelines can be found here: https://docs.nhcarrigan.com/#/contributing

View File

@ -1,5 +0,0 @@
# License
This software is licensed under our [global software license](https://docs.nhcarrigan.com/#/license).
Copyright held by Naomi Carrigan.

View File

@ -1,27 +1,36 @@
# Nhcarrigan This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.2. ## Getting Started
## Development server First, run the development server:
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
## Code scaffolding Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
## Build This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Learn More
## Running unit tests To learn more about Next.js, take a look at the following resources:
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
## Running end-to-end tests You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Deploy on Vercel
## Further help The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@ -1,3 +0,0 @@
# Security Policy
Our security policy can be found here: https://docs.nhcarrigan.com/#/security

View File

@ -1,3 +0,0 @@
# Terms of Service
Our Terms of Service can be found here: https://docs.nhcarrigan.com/#/terms

View File

@ -1,100 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"nhcarrigan": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser-esbuild",
"options": {
"outputPath": "dist/nhcarrigan",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.css"],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "nhcarrigan:build:production"
},
"development": {
"buildTarget": "nhcarrigan:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "nhcarrigan:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.css"],
"scripts": []
}
},
"deploy": {
"builder": "angular-cli-ghpages:deploy",
"options": {
"baseHref": "https://nhcarrigan.com",
"cname": "nhcarrigan.com",
"name": "Naomi Carrigan",
"email": "nhcarrigan@gmail.com"
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
}
}
}
}
},
"cli": {
"analytics": "7b13e4ca-046c-4eec-a791-b4c316f294e5"
}
}

View File

@ -1,47 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: "",
frameworks: ["jasmine", "@angular-devkit/build-angular"],
plugins: [
require("karma-jasmine"),
require("karma-chrome-launcher"),
require("karma-jasmine-html-reporter"),
require("karma-coverage"),
require("@angular-devkit/build-angular/plugins/karma"),
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
},
coverageReporter: {
dir: require("path").join(__dirname, "./coverage/profile"),
subdir: ".",
reporters: [{ type: "html" }, { type: "text-summary" }],
},
reporters: ["progress", "kjhtml"],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ["Chrome", "ChromeHeadless", "ChromeHeadlessCI"],
customLaunchers: {
ChromeHeadlessCI: {
base: "ChromeHeadless",
flags: ["--no-sandbox"],
},
},
singleRun: false,
restartOnFileChange: true,
});
};

8
next.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['cdn.nhcarrigan.com']
}
};
export default nextConfig;

View File

@ -1,50 +1,30 @@
{ {
"name": "nhcarrigan", "name": "portfolio",
"version": "0.0.0", "version": "0.1.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"lint": "ng lint && prettier src --check",
"test": "ng test --no-watch --no-progress --browsers=ChromeHeadlessCI",
"deploy": "ng deploy"
},
"private": true, "private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": { "dependencies": {
"@angular/animations": "17.3.5", "@fortawesome/fontawesome-svg-core": "6.6.0",
"@angular/common": "17.3.5", "@fortawesome/free-brands-svg-icons": "6.6.0",
"@angular/compiler": "17.3.5", "@fortawesome/free-solid-svg-icons": "6.6.0",
"@angular/core": "17.3.5", "@fortawesome/react-fontawesome": "0.2.2",
"@angular/forms": "17.3.5", "next": "14.2.6",
"@angular/platform-browser": "17.3.5", "react": "^18",
"@angular/platform-browser-dynamic": "17.3.5", "react-dom": "^18"
"@angular/router": "17.3.5",
"@fortawesome/angular-fontawesome": "0.14.1",
"@fortawesome/fontawesome-svg-core": "6.5.2",
"@fortawesome/free-brands-svg-icons": "6.5.2",
"@fortawesome/free-regular-svg-icons": "6.5.2",
"@fortawesome/free-solid-svg-icons": "6.5.2",
"rxjs": "7.8.1",
"tslib": "2.6.2",
"zone.js": "0.14.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "17.3.7", "@types/node": "^20",
"@angular/cli": "17.3.7", "@types/react": "^18",
"@angular/compiler-cli": "17.3.5", "@types/react-dom": "^18",
"@nhcarrigan/eslint-config-angular": "1.0.1", "eslint": "^8",
"@nhcarrigan/prettier-config": "3.2.0", "eslint-config-next": "14.2.6",
"@types/jasmine": "5.1.4", "postcss": "^8",
"angular-cli-ghpages": "1.0.7", "tailwindcss": "^3.4.1",
"eslint": "8.57.0", "typescript": "^5"
"jasmine-core": "5.1.2",
"karma": "6.4.3",
"karma-chrome-launcher": "3.2.0",
"karma-coverage": "2.2.1",
"karma-jasmine": "5.1.0",
"karma-jasmine-html-reporter": "2.1.0",
"prettier": "3.2.5",
"typescript": "5.4.5"
} }
} }

9353
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

1
public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

76
src/app/about/page.tsx Normal file
View File

@ -0,0 +1,76 @@
const About = (): JSX.Element => {
return (
<>
<main className="w-4/5 text-center max-w-4xl m-auto mt-16 mb-16 rounded-lg">
<h1 className="text-5xl">About Naomi</h1>
<section>
<p className="mb-2">
Passionate technologist dedicated to building inclusive tech
communities and empowering individuals to break into the field. With
a rich background in community management, software engineering, and
developer experience, I strive to create accessible pathways for
diverse talent.
</p>
<p className="mb-2">My expertise spans:</p>
<ul className="w-4/5 m-auto">
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
<strong>Inclusive Community Building:</strong> Cultivated
welcoming communities of 20,000 to 300K+ members across Discord,
Slack, and GitHub, with a focus on supporting underrepresented
groups in tech.
</li>
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
<strong>Empowering Education:</strong> Contributed to and
maintained open-source curricula used by millions of aspiring
developers globally, focusing on accessibility and engaging
learning experiences.
</li>
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
<strong>Software Engineering for Inclusivity:</strong> Developed
sophisticated bots and tools that not only streamline moderation
and boost engagement but also promote safe, inclusive spaces for
all community members.
</li>
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
<strong>Developer Experience (DX):</strong> Led initiatives to
improve documentation, SDK support, and overall developer
satisfaction, with an emphasis on making resources accessible to
newcomers in the field.
</li>
<li className="mb-2">
<strong>Mentorship and Advocacy:</strong> Implemented programs to
support aspiring developers, particularly those from
underrepresented backgrounds, in their journey into tech careers.
</li>
</ul>
<p className="mb-2">
Key achievements include redesigning freeCodeCamp&apos;s Responsive
Web Design curriculum for greater accessibility, developing
AI-powered community management tools that foster inclusive
interactions, and creating engagement systems that significantly
increased participation from diverse user groups.
</p>
<p className="mb-2">
I&apos;m driven by the belief that technology should be a field open
to all. My approach combines technical expertise with a deep
commitment to diversity, equity, and inclusion, resulting in
solutions that not only drive engagement and innovation but also
break down barriers to entry in tech.
</p>
<p className="mb-2">
Seeking opportunities to lead transformative projects that expand
access to tech education, foster inclusive community growth, and
empower individuals from all backgrounds to thrive in the tech
industry. Let&apos;s connect to discuss how I can best support your
organisation!
</p>
</section>
</main>
</>
);
};
export default About;

View File

@ -1,10 +0,0 @@
.glow {
position: fixed;
z-index: 1;
pointer-events: none;
height: 250px;
width: 250px;
background: radial-gradient(circle, #abfcecaa 0%, #00eeff00 50%);
background-repeat: no-repeat;
background-position: center;
}

View File

@ -1,7 +0,0 @@
<div class="content"><router-outlet></router-outlet></div>
<div
class="glow"
[style.left.px]="glowX"
[style.top.px]="glowY"
[style.display]="display"
></div>

View File

@ -1,90 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { AppComponent } from "./app.component";
import { Employment } from "./config/Employment";
import { Logos } from "./config/Logos";
import { Socials } from "./config/Socials";
describe("AppComponent", () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent]
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create the app", () => {
expect(component).toBeTruthy();
});
it(`should have the 'nhcarrigan' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual("nhcarrigan");
});
});
describe("Configs", () => {
it("employments should have correct oldest and newest", () => {
const sorted = Employment.sort(
(a, b) => b.start.getTime() - a.start.getTime()
);
const oldest = sorted.at(-1);
const newest = sorted[0];
expect(oldest?.title).toBe("Started Journey");
expect(newest.company).toBe("Your Company!");
});
it("employments should have valid dates", () => {
const sorted = [
...Employment.sort((a, b) => b.start.getTime() - a.start.getTime())
];
const oldest = sorted.pop();
const newest = sorted.shift();
if (!oldest || !newest) {
throw new Error("Failed to parse");
}
for (const job of sorted) {
expect(job.start.getTime())
.withContext(`${job.title} - ${job.company}`)
.toBeGreaterThan(oldest.start.getTime());
// Days should be set to the 5th - this ensures we're
// within the correct month in all timezones.
expect(job.start.getDate())
.withContext(`${job.title} - ${job.company}`)
.toBeGreaterThanOrEqual(4);
expect(job.start.getDate())
.withContext(`${job.title} - ${job.company}`)
.toBeLessThanOrEqual(6);
if (job.end) {
expect(job.end.getTime())
.withContext(`${job.title} - ${job.company}`)
.toBeLessThan(newest.start.getTime());
expect(job.end.getDate())
.withContext(`${job.title} - ${job.company}`)
.toBeGreaterThanOrEqual(4);
expect(job.end.getDate())
.withContext(`${job.title} - ${job.company}`)
.toBeLessThanOrEqual(6);
}
}
});
it("logos should be unique", () => {
const links = new Set(Logos.map((l) => l.link));
const names = new Set(Logos.map((l) => l.alt));
const files = new Set(Logos.map((l) => l.file));
expect(links.size).toBe(Logos.length);
expect(names.size).toBe(Logos.length);
expect(files.size).toBe(Logos.length);
});
it("socials should be unique", () => {
const names = new Set(Socials.map((s) => s.label));
const links = new Set(Socials.map((s) => s.link));
expect(links.size).toBe(Socials.length);
expect(names.size).toBe(Socials.length);
});
});

View File

@ -1,40 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component, HostListener } from "@angular/core";
import { RouterOutlet } from "@angular/router";
/**
*
*/
@Component({
selector: "app-root",
standalone: true,
imports: [CommonModule, RouterOutlet],
templateUrl: "./app.component.html",
styleUrl: "./app.component.css"
})
export class AppComponent {
title = "nhcarrigan";
glowX = 0;
glowY = 0;
display = "none";
/**
* Moves the sun when the mouse moves.
*
* @param {MouseEvent} e The mouse event.
*/
@HostListener("document:mousemove", ["$event"])
onMouseMove(e: MouseEvent) {
this.display = "block";
this.glowX = e.clientX - 125;
this.glowY = e.clientY - 125;
}
/**
* Hides the sun when the mouse leaves.
*/
@HostListener("document:mouseout", ["$event"])
onMouseOut() {
this.display = "none";
}
}

View File

@ -1,8 +0,0 @@
import { ApplicationConfig } from "@angular/core";
import { provideRouter } from "@angular/router";
import { routes } from "./app.routes";
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)]
};

View File

@ -1,5 +0,0 @@
import { Routes } from "@angular/router";
import { HomeComponent } from "./home/home.component";
export const routes: Routes = [{ path: "", component: HomeComponent }];

View File

@ -1,47 +0,0 @@
img {
width: 100%;
}
.desc {
max-width: 1200px;
margin: auto;
font-size: 1.5rem;
}
.certs {
display: grid;
grid-template-columns: repeat(3, 1fr);
row-gap: 10px;
width: 100%;
max-width: 1200px;
justify-items: center;
margin: auto;
}
.cert {
width: 300px;
}
.title {
font-size: 1.5rem;
font-weight: bold;
height: 4rem;
border: 2px solid var(--text);
}
.issued {
font-size: 1.25rem;
border: 2px solid var(--text);
}
@media screen and (max-width: 950px) {
.certs {
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (max-width: 650px) {
.certs {
grid-template-columns: repeat(1, 1fr);
}
}

View File

@ -1,19 +0,0 @@
<h2>Certifications</h2>
<p class="desc">
All of the professional certifications I have obtained throughout my journey.
</p>
<div class="certs">
<div class="cert" *ngFor="let cert of certs">
<p class="title">{{ cert.name }}</p>
<img src="/assets/img/certs/{{ cert.fileName }}" />
<p class="issued">
{{ cert.issuer }} -
{{
cert.date.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
})
}}
</p>
</div>
</div>

View File

@ -1,49 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Certifications } from "../config/Certifications";
import { CertificationsComponent } from "./certifications.component";
describe("CertificationsComponent", () => {
let component: CertificationsComponent;
let fixture: ComponentFixture<CertificationsComponent>;
let compiled: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CertificationsComponent]
}).compileComponents();
fixture = TestBed.createComponent(CertificationsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.nativeElement;
});
it("should load the certifications", () => {
expect(component.certs).toEqual(
Certifications.sort((a, b) => b.date.getTime() - a.date.getTime())
);
});
it("should render the certifications correctly", () => {
const sorted = Certifications.sort(
(a, b) => b.date.getTime() - a.date.getTime()
);
const certs = compiled.querySelectorAll(".cert");
for (let i = 0; i < certs.length; i++) {
const cert = certs[i];
const title = cert.querySelector(".title");
expect(title?.textContent?.trim()).toBe(sorted[i].name);
const img = cert.querySelector("img");
expect(img?.src).toContain(`/assets/img/certs/${sorted[i].fileName}`);
const issued = cert.querySelector(".issued");
expect(issued?.textContent?.trim()).toBe(
`${sorted[i].issuer} - ${sorted[i].date.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
})}`
);
}
});
});

View File

@ -1,20 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Certifications } from "../config/Certifications";
/**
*
*/
@Component({
selector: "app-certifications",
standalone: true,
imports: [CommonModule],
templateUrl: "./certifications.component.html",
styleUrl: "./certifications.component.css"
})
export class CertificationsComponent {
public certs = Certifications.sort(
(a, b) => b.date.getTime() - a.date.getTime()
);
}

36
src/app/certs/page.tsx Normal file
View File

@ -0,0 +1,36 @@
import { Certification } from "@/components/cert";
import { Rule } from "@/components/rule";
import { Certifications } from "@/config/Certifications";
import { Charm } from "next/font/google";
const Certs = (): JSX.Element => {
return (
<>
<main className="w-[95%] text-center max-w-4xl m-auto mt-16 mb-16 rounded-lg">
<h1 className="text-5xl">Certifications</h1>
<section>
<p className="mb-2">
This page lists the professional certifications Naomi has obtained
throughout her time as a developer.
</p>
<Rule />
<div className="grid sm:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-y-5">
{Certifications.sort(
(a, b) => b.date.getTime() - a.date.getTime(),
).map((cert) => (
<Certification
key={cert.name}
name={cert.name}
fileName={cert.fileName}
issuer={cert.issuer}
date={cert.date}
/>
))}
</div>
</section>
</main>
</>
);
};
export default Certs;

View File

@ -1,361 +0,0 @@
export const Employment: {
title: string;
company: string;
/**
* Set ALL dates to the 5th. These are not exact days, but intended to avoid month discrepancies
* when dealing with timezones.
*/
start: Date;
/**
* Set ALL dates to the 5th. These are not exact days, but intended to avoid month discrepancies
* when dealing with timezones.
*/
end: Date | null;
link: string;
type: "volunteer" | "fixed" | "project" | "hypothetical";
description: string;
/**
* File name of logo.
*/
logo: string;
}[] = [
{
title: "Consultant",
company: "Your Company!",
start: new Date(Date.now()),
end: null,
link: "https://topmate.io/nhcarrigan/913920",
type: "hypothetical",
logo: "future.jpeg",
description: `With a deep passion for leveraging technology for positive social impact, I am a community-oriented software engineer dedicated to cultivating safe, inclusive, and welcoming online spaces. My focus on user experience and accessibility drives me to create robust software tools and platforms prioritizing privacy, security, and diversity.
My career reflects a commitment to supporting aspiring developers as they break into the tech industry. Through mentorship, educational initiatives, and advocating for inclusive hiring practices, I strive to remove barriers and empower underrepresented individuals in the digital realm.
For me, software engineering goes beyond codingit's about using technology to drive positive change. By championing diversity, equity, and accessibility in every project, I aim to reshape the tech industry's narrative and contribute to a more equitable future.
Let's connect to drive meaningful impact and foster innovation and inclusivity in technology.`
},
{
title: "Started Journey",
company: "nhcarrigan",
start: new Date("April 5 2020"),
end: null,
link: "https://naomi.lgbt",
type: "hypothetical",
logo: "nhcarrigan.jpeg",
description:
"Began my journey learning to code, starting from the top of the freeCodeCamp curriculum."
},
{
title: "Development Lead",
company: "Artists For Palestine",
start: new Date("November 5 2023"),
end: null,
link: "https://art4palestine.org",
type: "volunteer",
logo: "a4p.jpeg",
description: `As a Development Lead at Art 4 Palestine, I developed a bot that efficiently manages integrations between Airtable forms, Trello boards, and Discord, streamlining workflow and improving productivity. The bot also pulls news articles from reputable sources and cross-posts them into the server, keeping the community informed with reliable updates.
Additionally, I guided and mentored other developers in the design and coding of the website, ensuring high-quality output and consistent alignment with project goals. My contributions have been instrumental in supporting the organization's initiatives and fostering collaboration within the development team.`
},
{
title: "Community Moderator",
company: "AngelRose",
start: new Date("September 5 2023"),
end: null,
link: "https://discord.gg/kYpjgEB",
type: "volunteer",
logo: "angel.png",
description: `As a Discord Moderator at AngelRose, I played a key role in maintaining a safe, respectful, and engaging environment for community members. I monitored conversations, enforcing community guidelines to prevent disruptive behavior and protect users. Additionally, I addressed and resolved conflicts, providing support and guidance to members to promote a positive experience. My contributions helped uphold the integrity of the community and foster an inclusive and welcoming space for all participants.`
},
{
title: "Community Bot Engineer",
company: "Deepgram",
start: new Date("July 5 2023"),
end: new Date("June 4 2024"),
link: "https://deepgram.com",
type: "project",
logo: "deepgram.jpeg",
description: `As a Community Bot Engineer at Deepgram, I developed a sophisticated Discord bot to enhance community management and streamline user interactions. The bot features an array of capabilities, including the ability to move messages from standard channels to forum channels, maintaining an organized space for discussion. It integrates AI to automatically answer new questions in the forum channel and allows for responses to be marked as correct answers.
I designed the bot to send scheduled messages in the general channel, prompting users to utilize the forum channel for their questions. Additionally, the bot provides daily and weekly aggregations of unanswered questions, cross-posts answered questions to the GitHub discussion board, and reminds users of unanswered questions if they've been inactive for seven days.
The bot also tracks various metrics such as unanswered, answered, and closed questions, as well as questions moved by the bot, to monitor the overall health of the community. Finally, it can post feedback messages to ProductBoard, contributing to product development and user satisfaction.
Through these innovative features, my work has improved community engagement and provided a seamless, organized experience for Deepgram's users.`
},
{
title: "Twitch Integration Engineer",
company: "BigBadBeaver Productions",
start: new Date("May 5 2023"),
end: new Date("January 5 2024"),
link: "https://linktr.ee/bigbadbeaver",
type: "project",
logo: "beaver.png",
description: `As a Twitch Integration Engineer at Big Bad Beaver Productions, I developed "PrivateTwigs," a custom Twitch chat bot that enhanced stream management and engagement. The bot includes comprehensive logging for all chat messages and stream events, enabling detailed analysis and strategic insights.
Additionally, I implemented custom redemption rewards within the bot, providing viewers with unique opportunities to interact with the stream and engage with content. My work as a Twitch Integration Engineer significantly improved the streaming experience for both streamers and viewers, contributing to a more interactive and dynamic community.`
},
{
title: "Community Manager and Open-Source Engineer",
company: "Sema Software",
start: new Date("May 5 2022"),
end: new Date("September 5 2022"),
link: "https://www.semasoftware.com",
type: "fixed",
logo: "sema.jpeg",
description: `In my role as Community Manager and Open Source Engineer at Sema Software, I significantly shaped and cultivated the Discord community, expanding its membership from 300 to 1,000. This involved fostering engagement and creating a welcoming space for all members.
In addition to community management, I led various open source initiatives across multiple projects. This included creating a base template for starting new projects and providing developers with a structured and efficient foundation. I also managed the Developer Skills Matrix repository and its associated Discord bot, website, and Slack bot, allowing users to take self-assessments across their preferred platforms.
Moreover, I developed a Discord bot to query projects listed on the Open Source Welcoming Committee, enhancing accessibility and visibility for open source projects. Through these contributions, I have played a key role in driving community growth and promoting open source engagement at Sema Software.`
},
{
title: "Community Manager",
company: "4C",
start: new Date("May 5 2022"),
end: new Date("November 5 2022"),
link: "https://discord.com/invite/ns5x8bTz25",
type: "fixed",
logo: "4c.png",
description: `As Community Manager at 4C, I played a pivotal role in shaping and nurturing the Discord community, driving significant growth and member engagement. Under my management, the community expanded from 1,100 to 3,350 members, fostering an active and thriving space for all participants. My contributions have been instrumental in creating a welcoming and dynamic environment, promoting meaningful interactions, and enhancing overall community satisfaction.`
},
{
title: "Community Manager",
company: "TweetShift",
start: new Date("January 5 2022"),
end: new Date("May 5 2023"),
link: "https://tweetshift.com",
type: "fixed",
logo: "tweetshift.png",
description: `As the Community Manager for the TweetShift Discord bot, I provided comprehensive support to a community that utilizes the bot across over 230,000 servers. My focus was on delivering high-quality assistance and ensuring the bot's seamless functionality for users. I also played a key role in moderating the community and maintaining a safe and inclusive environment for all members. Through these efforts, I have fostered a welcoming space and contributed to the overall success and engagement of the TweetShift community.`
},
{
title: "Senior Integrations Engineer",
company: "Rythm",
start: new Date("April 5 2022"),
end: null,
link: "https://rythm.fm",
type: "fixed",
description: `As a Senior Integrations Engineer at Rythm, I specialized in building Discord bots and tools that empower our team to effectively manage a server of over 300,000 members. My work involved creating a comprehensive moderation bot that includes an auto-moderation system, extensive moderation history, and FAQ responses. This tool offers individual case management, evidence logging, a staff-only note system, and customizable automod rules.
I developed an economy bot that rewards long-term activity with levels, roles, and a currency system that enables members to purchase specialized rewards, personalize their bot profiles, and earn badges. Additionally, I designed a staff incentive bot that allows leadership to recognize volunteer moderators and staff for their exceptional contributions.
My projects also include an analytics bot to monitor user engagement, a trivia bot to entertain members with music-related trivia questions, and a social media bot that cross-posts Twitter updates to the Discord community and internal Slack. Furthermore, I implemented a staff analytics bot to track staff performance, encompassing commands used, messages sent, and modmail threads closed.
Through these tools and bots, I have greatly enhanced the efficiency and engagement within the Rythm server, fostering a vibrant and interactive community.`,
logo: "rythm.jpeg"
},
{
title: "Community Manager and Infrastructure Engineer",
company: "Streamcord",
start: new Date("August 5 2021"),
end: null,
link: "https://streamcord.io",
type: "fixed",
logo: "streamcord.jpeg",
description: `In my role as Community Manager and Infrastructure Engineer at Streamcord, I managed a vibrant community of approximately 50,000 members. I provided support for the Streamcord bot, which is actively used in over 1 million communities, ensuring its seamless operation and addressing user queries. My initiatives included running staff streams and game nights to foster interaction and strengthen the bond between the community and the team. I played a key role in overhauling the community to boost retention and activity, and supported the development process, contributing to the maintenance of the documentation and dashboard site. Additionally, I took charge of interviewing and onboarding new staff, providing comprehensive training, and conducting quarterly evaluations to ensure a skilled and cohesive team. My work has greatly contributed to the thriving and dynamic environment within the Streamcord community.`
},
{
title: "Community Moderator",
company: "Battlesnake",
start: new Date("June 5 2021"),
end: new Date("November 5 2022"),
link: "https://play.battlesnake.com",
type: "volunteer",
logo: "battlesnake.jpeg",
description: `As a Community Moderator at Battlesnake, I played a vital role in ensuring a safe and welcoming environment for players. My focus was on promoting a positive experience for all members, allowing them to enjoy the game and interact with one another respectfully.
Additionally, I contributed to driving engagement for the Summer League 2021 and the Caster House system, fostering enthusiasm and participation in these events. My efforts helped create an inclusive and vibrant community for Battlesnake enthusiasts.`
},
{
title: "Discord Administrator and Platform Engineering Manager",
company: "Caylus Crew",
start: new Date("June 5 2021"),
end: null,
link: "https://discord.gg/infinite",
type: "volunteer",
logo: "caylus.png",
description: `As the Discord Administrator and Platform Engineering Manager at Caylus Crew, I developed custom bots that enhanced the community experience and streamlined operations. One such bot posted daily messages wishing happy birthday to members, fostering a sense of connection and camaraderie. I also created a bot to manage sponsor perks, ensuring efficient and accurate distribution of benefits to eligible members.
In addition to technical contributions, I coached and trained moderators, providing quarterly staff reviews to support their professional growth and improve performance. My work played a pivotal role in maintaining a vibrant, well-managed community and ensuring an exceptional experience for all participants.`
},
{
title: "Integrations Engineer",
company: "Xcentric Collective",
start: new Date("April 5 2021"),
end: new Date("July 5 2023"),
link: "http://discord.gg/U3jQVYNbJt",
type: "volunteer",
logo: "xcentric.jpg",
description: `As an Integrations Engineer at Xcentric Collective, I developed a custom Discord bot that incorporated a unique Matchmaking Rating (MMR) system to calculate Rocket League proficiency. This system enabled users to track and monitor their skill levels accurately.
The bot also featured the ability to define teams and find matches with opponents of similar skill levels, facilitating balanced and fair gameplay. Additionally, the bot allowed users to schedule matches seamlessly, providing a streamlined and efficient experience for the community. My work enhanced the overall gaming experience and engagement within the Xcentric Collective community. As an Integrations Engineer at Xcentric Collective, I developed a custom Discord bot that incorporated a unique Matchmaking Rating (MMR) system to calculate Rocket League proficiency. This system enabled users to track and monitor their skill levels accurately. The bot also featured the ability to define teams and find matches with opponents of similar skill levels, facilitating balanced and fair gameplay. Additionally, the bot allowed users to schedule matches seamlessly, providing a streamlined and efficient experience for the community. My work enhanced the overall gaming experience and engagement within the Xcentric Collective community.`
},
{
title: "Hacktoberfest Community Moderator",
company: "DigitalOcean",
start: new Date("April 5 2021"),
end: null,
link: "https://hacktoberfest.com",
type: "volunteer",
logo: "digitalocean.jpeg",
description: `As a Hacktoberfest Community Moderator at DigitalOcean, I supported the community by building a custom bot to ensure repository links were correctly included in promotional channel messages, streamlining interactions, and providing automated responses for frequently asked questions. This contributed to a smoother experience for participants and enhanced community engagement.
In addition to managing the bot, I answered queries and guided developers in their open-source contributions, helping them navigate the Hacktoberfest event and maximize their impact. By maintaining a safe and welcoming community space, I fostered an inclusive environment for developers of all skill levels to collaborate and grow. My efforts played a crucial role in the success of the event and the satisfaction of its participants.`
},
{
title: "Discord Administrator",
company: "EddieHub",
start: new Date("January 5 2021"),
end: new Date("May 5 2023"),
link: "https://www.eddiehub.org",
type: "volunteer",
logo: "eddiehub.jpeg",
description: `As a Discord Administrator for EddieHub, I played a key role in moderating the community and fostering an open and inclusive environment. My focus was on upholding community guidelines and ensuring a positive experience for all members.
I provided support and encouragement to members on their development path, offering guidance and resources to help them grow and succeed. Additionally, I planned and managed community events and initiatives, creating opportunities for members to engage, learn, and collaborate.
My efforts contributed to the vibrancy and success of the EddieHub community, helping to create a welcoming space for developers to thrive.`
},
{
title: "Discord Administrator and Lead Integrations Engineer",
company: "Commit Your Code",
start: new Date("December 5 2020"),
end: null,
link: "https://discord.gg/StwJYeq",
type: "volunteer",
logo: "cyc.jpeg",
description: `As the Discord Administrator and Lead Integrations Engineer at Commit Your Code, I played a key role in fostering a supportive and friendly community environment by moderating interactions and ensuring a positive space for members. I provided guidance to individuals on their development path, offering support as they prepared for their first job or encountered coding challenges.
To enhance community security, I built a robust verification system that significantly reduced the number of compromised accounts, safeguarding members' data and promoting a safe space for collaboration. My contributions have been instrumental in supporting members' growth and maintaining a secure, welcoming atmosphere within the community.`
},
{
title: "Educational Developer and Community Manager",
company: "freeCodeCamp",
start: new Date("December 5, 2020"),
end: null,
link: "https://freecodecamp.org",
type: "fixed",
logo: "fcc.jpeg",
description: `As an Educational Developer and Community Manager at freeCodeCamp, I have played a pivotal role in maintaining and enhancing the platform's open-source curriculum, serving millions of developers globally. I assisted in completely redesigning the Responsive Web Design curriculum to create a more accessible and engaging learning experience. Additionally, I built a moderation bot to improve the safety and functionality of the Discord community and developed a tool to efficiently distribute a weekly email newsletter to millions of subscribers. My work also involves supporting and guiding users on their journey to become web developers while managing the Discord, Forum, and Reddit communities to drive engagement and deliver an exceptional user experience. Through these contributions, I have significantly impacted the growth and success of the freeCodeCamp community, empowering individuals worldwide to achieve their web development goals. As an Educational Developer and Community Manager at freeCodeCamp, I have played a pivotal role in maintaining and enhancing the platform's open-source curriculum, serving millions of developers globally. I assisted in completely redesigning the Responsive Web Design curriculum to create a more accessible and engaging learning experience. Additionally, I built a moderation bot to improve the safety and functionality of the Discord community and developed a tool to efficiently distribute a weekly email newsletter to millions of subscribers. My work also involves supporting and guiding users on their journey to become web developers while managing the Discord, Forum, and Reddit communities to drive engagement and deliver an exceptional user experience. Through these contributions, I have significantly impacted the growth and success of the freeCodeCamp community, empowering individuals worldwide to achieve their web development goals.`
},
{
title: "Discord Moderator",
company: "Virtual Insanity",
start: new Date("May 5 2024"),
end: null,
link: "https://discord.com/invite/GDYNGnrGUs",
type: "volunteer",
logo: "troopy.png",
description: `As a Discord Moderator for Virtual Insanity, an adult-only community, I was instrumental in cultivating a secure, respectful, and interactive atmosphere for our diverse members. My responsibilities encompassed vigilant monitoring of discussions, ensuring compliance with established guidelines to deter any disruptive conduct, and safeguarding our users. Alongside conflict resolution, I provided empathetic support and guidance, fostering a harmonious environment conducive to positive interactions. Moreover, I actively assisted in verifying identification documents, reinforcing our commitment to maintaining a safe and authentic community experience. Through these efforts, I contributed to upholding the community's integrity and nurturing an inclusive space for all participants to thrive.`
},
{
title: "Discord Administrator and Integrations Engineer",
company: "Azuliah (VTuber)",
start: new Date("December 5 2023"),
end: new Date("April 5 2024"),
link: "https://discord.com/invite/XNSy8PMvyy",
type: "volunteer",
logo: "azuliah.jpg",
description: `As a Discord Administrator and Integrations Engineer at Azuliah, I established custom integrations to streamline moderation efforts and enhance community management. These integrations supported the moderation team in maintaining a safe and welcoming space for all members.
In addition to technical contributions, I trained the owner and moderation team on best practices for running a successful community. My guidance included techniques for efficient moderation, conflict resolution, and fostering positive interactions among members. Through my efforts, I played a key role in ensuring the community's smooth operation and promoting a vibrant, supportive environment.`
},
{
title: "Discord Moderator",
company: "Rion Kuroko (VTuber)",
start: new Date("Nov 5 2023"),
end: new Date("Jan 5 2024"),
link: "https://discord.com",
type: "volunteer",
logo: "rion.jpg",
description: `As a Discord Moderator for Rion Kuroko, I constructed the server almost entirely from scratch to tailor it to the specific needs of the community. This involved designing and implementing structures, channels, and rules that supported a smooth and organized environment.
I also provided guidance to the owner on the technical aspects of Discord moderation, sharing best practices and offering solutions to effectively manage the server. My efforts played a key role in establishing a functional, user-friendly community space and empowering the owner with the knowledge needed to maintain and grow the server.`
},
{
title: "Senior Discord Moderator",
company: "Rythm",
start: new Date("Feb 5 2022"),
end: new Date("July 5 2022"),
link: "https://discord.com/invite/rythm",
type: "volunteer",
logo: "rythm.jpeg",
description: `As a Senior Discord Moderator for Rythm, I played a dual role in overseeing community moderation and mentoring the moderation team. My responsibilities included tracking weekly staff activity to monitor performance and identify areas for improvement, ensuring the team remained effective and engaged.
Additionally, I guided and supported moderators, helping them develop their skills and excel in their roles. By identifying opportunities to enhance staff activity, I contributed to the overall health and vibrancy of the community. My efforts were instrumental in maintaining a positive, safe, and well-managed space for all members.`
},
{
title: "Technical Support Staff",
company: "TweetShift",
start: new Date("Oct 5 2021"),
end: new Date("Jan 5 2022"),
link: "https://discord.com/invite/zdfQhjc",
type: "volunteer",
logo: "tweetshift.png",
description: `As Technical Support Staff for TweetShift, I responded promptly to user queries, addressing issues and bugs within the bot to ensure a smooth user experience. I provided clear guidance on how to use the bot's features effectively, helping users maximize its capabilities.
My role involved troubleshooting technical challenges and offering solutions to enhance user satisfaction. By delivering efficient support and sharing helpful tips, I contributed to the overall success and reliability of the bot for the TweetShift community.`
},
{
title: "Discord Moderator",
company: "Rythm",
start: new Date("Sept 5 2021"),
end: new Date("Feb 5 2022"),
link: "https://discord.com/invite/rythm",
type: "volunteer",
logo: "rythm.jpeg",
description: `As a Discord Moderator for Rythm, I played a central role in maintaining a safe, respectful, and welcoming environment for community members. I monitored conversations to ensure compliance with community guidelines and addressed any disruptive behavior promptly.
My efforts helped foster a positive and inclusive space for all members, contributing to the overall health and vibrancy of the Rythm community.`
},
{
title: "Technical Support Staff",
company: "Streamcord",
start: new Date("Mar 5 2021"),
end: new Date("Aug 5 2021"),
link: "https://discord.com/invite/streamcord",
type: "volunteer",
logo: "streamcord.jpeg",
description: `As Technical Support Staff for Streamcord, I provided essential support to users by triaging and debugging issues related to the Streamcord Discord bot. My role involved investigating and resolving technical challenges to ensure a seamless user experience.
Acting as a liaison between users and developers, I facilitated clear communication and reported user feedback to the development team for continuous improvement. Additionally, I moderated and engaged with the community, fostering a positive and inclusive environment for all participants. My efforts contributed to the overall success and satisfaction of the Streamcord community.`
},
{
title: "Community Moderator",
company: "freeCodeCamp",
start: new Date("Jun 5 2020"),
end: new Date("Dec 5 2020"),
link: "https://discord.com/invite/freecodecamp-org-official-fi-fo-692816967895220344",
type: "volunteer",
logo: "fcc.jpeg",
description: `As a Community Moderator for freeCodeCamp, I provided vital support to users (campers) as they navigated the freeCodeCamp curriculum. My role included assisting users in debugging their code and answering questions, ensuring they received the guidance they needed to progress in their learning journey.
I engaged with and moderated the community on the forum and Discord server, fostering a positive, supportive, and inclusive environment for all members. Additionally, I assisted with issue triage and pull request review on GitHub, contributing to the ongoing improvement and development of freeCodeCamp's open-source projects. My efforts played a key role in maintaining the quality of the community and empowering users to achieve their learning goals.`
},
{
title: "Community Moderator",
company: "FruitPursuits",
start: new Date("March 5 2024"),
end: null,
link: "https://discord.gg/xcy2fRsC5K",
type: "volunteer",
logo: "fruit.png",
description: `As a Discord Moderator for FruitPursuits, I provide crucial support to members, ensuring they have a seamless experience within our community. I troubleshoot technical issues, enforce community guidelines, and foster engagement among members. Collaborating with fellow moderators, I contribute to the ongoing improvement of our Discord server, empowering users to fully enjoy their fruit-loving journey.`
},
{
title: "Developer Experience Consultant",
company: "Deepgram",
start: new Date("June 5 2024"),
end: null,
link: "https://deepgram.com",
type: "fixed",
logo: "deepgram.jpeg",
description: `As a DX Consultant at Deepgram, I spearhead efforts to enhance community engagement and streamline developer experiences. Leveraging my expertise, I optimize community management workflows on platforms like Slack, GitHub Discussions, and Discord. My primary focus involves acting as a Community Manager, where I strive to answer approximately 60% of community inquiries, foster engagement, and ensure valuable feedback reaches the Product Board for consideration.
In parallel, I lead DX initiatives, delivering minor documentation improvements, documenting new product features, and integrating new solutions. My proficiency extends to SDK support (JS, Python), where I deliver enhancements and develop new applications as needed. Additionally, I provide high-priority support for demo applications.
Through my multifaceted approach, I drive tangible improvements in community interactions and developer experiences, contributing to Deepgram's growth and user satisfaction.`
}
];

View File

@ -1,957 +0,0 @@
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { faCalendar, faCircle } from "@fortawesome/free-regular-svg-icons";
import { faListCheck } from "@fortawesome/free-solid-svg-icons";
import { Employment } from "./Employment";
export const Codeberg: IconDefinition = {
prefix: "xxx",
iconName: "yyy",
icon: [
474,
474,
[],
"U+E002",
`M 36.00,373.00
C 19.99,344.87 11.56,324.72 5.00,293.00
5.00,293.00 2.08,280.00 2.08,280.00
2.08,280.00 2.08,271.04 2.08,271.04
1.21,265.55 0.02,268.68 0.00,259.00
0.00,259.00 0.00,232.00 0.00,232.00
0.10,225.30 1.32,226.49 2.08,221.96
2.08,221.96 2.08,214.00 2.08,214.00
2.08,214.00 4.55,203.00 4.55,203.00
8.89,181.33 13.79,163.93 23.75,144.00
23.75,144.00 31.87,128.00 31.87,128.00
40.56,112.80 56.86,91.48 69.17,79.17
69.17,79.17 80.00,69.72 80.00,69.72
90.99,60.09 102.48,51.08 115.00,43.46
115.00,43.46 132.00,34.75 132.00,34.75
194.09,3.71 265.92,2.29 330.00,28.45
346.58,35.22 368.94,48.49 383.00,59.51
383.00,59.51 397.00,71.72 397.00,71.72
418.24,90.34 435.64,113.79 448.25,139.00
460.07,162.66 464.56,176.20 469.80,202.00
471.25,209.15 473.99,218.02 474.00,225.00
474.00,225.00 474.00,270.00 474.00,270.00
474.00,270.00 469.42,294.00 469.42,294.00
469.42,294.00 465.25,313.00 465.25,313.00
465.25,313.00 454.33,343.00 454.33,343.00
454.33,343.00 439.00,373.00 439.00,373.00
439.00,373.00 437.00,373.00 437.00,373.00
437.00,373.00 386.42,307.00 386.42,307.00
386.42,307.00 285.87,177.00 285.87,177.00
285.87,177.00 250.35,131.00 250.35,131.00
247.89,127.81 240.50,116.27 236.17,117.49
234.17,118.05 231.85,121.40 230.58,123.00
230.58,123.00 221.13,135.00 221.13,135.00
221.13,135.00 183.35,184.00 183.35,184.00
183.35,184.00 88.88,306.00 88.88,306.00
88.88,306.00 55.65,349.00 55.65,349.00
50.94,355.09 41.27,368.61 36.00,373.00 Z
M 244.00,138.00
C 244.00,138.00 272.65,174.00 272.65,174.00
272.65,174.00 327.65,245.00 327.65,245.00
327.65,245.00 401.88,341.00 401.88,341.00
401.88,341.00 433.00,382.00 433.00,382.00
433.00,382.00 409.96,410.00 409.96,410.00
409.96,410.00 400.00,419.17 400.00,419.17
400.00,419.17 389.00,429.68 389.00,429.68
389.00,429.68 361.00,449.54 361.00,449.54
361.00,449.54 346.00,457.31 346.00,457.31
346.00,457.31 331.00,465.00 331.00,465.00
331.00,465.00 315.85,409.00 315.85,409.00
315.85,409.00 283.85,289.00 283.85,289.00
283.85,289.00 257.12,189.00 257.12,189.00
257.12,189.00 244.00,138.00 244.00,138.00 Z`
]
} as never;
export const Matrix: IconDefinition = {
prefix: "xxx",
iconName: "yyy",
icon: [
342,
342,
[],
"U+E002",
`M 138.00,0.53
C 150.87,-1.59 166.46,0.98 179.00,4.13
230.88,17.15 270.41,61.51 279.08,114.00
279.08,114.00 281.00,131.00 281.00,131.00
281.08,137.92 281.47,143.03 277.20,148.99
268.97,160.46 252.22,161.04 243.39,149.98
237.90,143.12 239.56,138.59 238.83,131.00
238.25,124.97 237.10,118.87 235.63,113.00
230.45,92.32 217.03,73.13 200.00,60.48
188.68,52.07 173.95,45.62 160.00,43.28
149.18,41.47 136.43,43.78 128.21,34.91
117.46,23.31 123.25,5.13 138.00,0.53 Z
M 126.00,61.42
C 132.79,60.60 140.80,60.41 147.00,63.67
157.30,69.10 161.07,83.05 155.00,93.00
148.91,102.97 142.67,102.25 133.00,103.17
133.00,103.17 121.00,104.75 121.00,104.75
106.64,107.11 91.56,113.68 80.00,122.50
61.64,136.52 50.33,153.71 44.63,176.00
43.46,180.57 42.62,185.30 42.17,190.00
41.42,197.85 43.18,203.96 37.61,210.99
28.88,221.99 10.70,222.28 2.99,210.00
-0.87,203.84 -0.08,195.99 0.00,189.00
0.10,181.05 1.94,171.72 3.87,164.00
14.25,122.51 43.23,89.80 82.00,72.31
97.65,65.25 109.47,63.78 126.00,61.42 Z
M 315.00,123.64
C 330.97,120.43 341.97,128.41 342.00,145.00
342.03,164.57 338.91,182.06 330.69,200.00
308.91,247.57 260.01,281.64 207.00,281.00
183.02,280.71 177.62,255.16 192.04,243.39
198.72,237.94 203.50,239.55 211.00,238.83
217.03,238.25 223.12,237.09 229.00,235.63
249.81,230.42 269.96,216.52 282.28,199.00
290.55,187.24 296.09,175.19 298.74,161.00
301.56,145.87 296.54,130.60 315.00,123.64 Z
M 78.00,184.53
C 87.56,183.00 96.80,186.96 101.08,196.00
101.08,196.00 105.45,225.00 105.45,225.00
108.31,239.01 115.29,252.68 123.90,264.00
136.86,281.04 157.21,293.39 178.00,298.11
188.70,300.54 202.89,298.40 210.98,304.56
222.59,313.38 221.84,332.30 209.00,339.64
203.20,342.95 194.60,342.07 188.00,342.00
180.39,341.91 168.41,339.51 161.00,337.42
119.62,325.74 86.75,295.86 70.60,256.00
70.60,256.00 66.29,243.00 66.29,243.00
63.87,234.58 62.76,227.67 61.84,219.00
60.14,203.20 59.71,189.94 78.00,184.53 Z`
]
} as never;
export const Peerlist: IconDefinition = {
prefix: "xxx",
iconName: "yyy",
icon: [
512,
512,
[],
"U+E002",
`M 214.00,0.14
C 214.00,0.14 243.00,0.14 243.00,0.14
243.00,0.14 291.00,0.14 291.00,0.14
291.00,0.14 301.00,1.00 301.00,1.00
301.00,1.00 311.00,1.00 311.00,1.00
311.00,1.00 328.00,2.83 328.00,2.83
375.51,7.58 421.85,17.89 457.91,51.42
483.50,75.23 497.13,112.43 503.80,146.00
507.93,166.77 510.12,187.89 511.04,209.00
511.04,209.00 512.00,220.00 512.00,220.00
512.59,270.71 513.41,322.24 502.42,372.00
495.29,404.31 483.17,434.75 459.72,458.83
437.47,481.67 407.37,493.82 377.00,501.37
326.73,513.88 274.38,512.08 223.00,512.00
223.00,512.00 208.00,511.00 208.00,511.00
208.00,511.00 204.00,511.00 204.00,511.00
204.00,511.00 166.00,507.13 166.00,507.13
125.75,501.54 81.42,488.74 52.28,458.83
18.32,423.96 7.84,375.75 2.84,329.00
2.84,329.00 0.91,302.00 0.91,302.00
0.91,302.00 0.00,290.00 0.00,290.00
0.00,290.00 0.00,253.00 0.00,253.00
0.00,253.00 0.00,223.00 0.00,223.00
0.00,223.00 1.00,205.00 1.00,205.00
1.00,205.00 6.59,155.00 6.59,155.00
11.95,123.04 22.32,89.63 42.81,64.00
74.31,24.60 121.91,10.61 170.00,4.28
170.00,4.28 201.00,1.09 201.00,1.09
201.00,1.09 214.00,0.14 214.00,0.14 Z
M 234.00,18.14
C 227.52,19.22 223.42,18.99 217.00,19.00
217.00,19.00 205.00,20.04 205.00,20.04
205.00,20.04 197.00,20.04 197.00,20.04
149.57,24.69 92.98,33.42 60.44,72.00
34.70,102.51 25.98,143.28 21.84,182.00
21.84,182.00 19.00,224.00 19.00,224.00
19.00,224.00 18.00,241.00 18.00,241.00
18.00,241.00 18.00,273.00 18.00,273.00
18.00,273.00 18.96,283.00 18.96,283.00
18.96,283.00 18.96,297.00 18.96,297.00
18.96,297.00 19.91,307.00 19.91,307.00
19.91,307.00 24.92,353.00 24.92,353.00
30.18,384.75 40.74,418.98 63.09,443.00
93.04,475.19 139.93,485.66 182.00,490.16
182.00,490.16 205.00,492.09 205.00,492.09
205.00,492.09 215.00,493.04 215.00,493.04
215.00,493.04 229.00,493.04 229.00,493.04
229.00,493.04 241.00,494.00 241.00,494.00
241.00,494.00 271.00,494.00 271.00,494.00
271.00,494.00 283.00,493.04 283.00,493.04
283.00,493.04 295.00,493.04 295.00,493.04
295.00,493.04 307.00,492.09 307.00,492.09
327.86,490.66 348.55,488.59 369.00,483.88
398.65,477.06 426.42,466.49 447.96,444.00
469.93,421.06 480.00,389.49 486.00,359.00
490.02,338.59 492.97,308.77 493.00,288.00
493.00,288.00 494.00,271.00 494.00,271.00
494.00,271.00 494.00,241.00 494.00,241.00
494.00,241.00 493.04,229.00 493.04,229.00
493.04,229.00 493.04,217.00 493.04,217.00
493.04,217.00 492.09,205.00 492.09,205.00
490.82,186.52 489.06,168.22 485.40,150.00
478.89,117.63 467.20,84.85 442.00,62.17
412.18,35.33 368.80,25.99 330.00,21.84
330.00,21.84 307.00,19.91 307.00,19.91
307.00,19.91 295.00,18.96 295.00,18.96
295.00,18.96 283.00,18.96 283.00,18.96
283.00,18.96 271.00,18.14 271.00,18.14
271.00,18.14 234.00,18.14 234.00,18.14 Z
M 156.00,101.00
C 156.00,101.00 231.00,101.00 231.00,101.00
231.00,101.00 270.00,101.00 270.00,101.00
304.65,101.05 338.60,117.93 361.91,143.00
370.38,152.10 378.11,164.62 383.14,176.00
398.86,211.57 395.05,254.17 374.58,287.00
365.39,301.73 358.75,306.40 347.28,318.17
331.52,334.35 322.21,343.76 301.00,353.14
279.20,362.77 264.27,364.00 241.00,364.00
241.00,364.00 241.00,411.00 241.00,411.00
241.00,411.00 223.00,411.00 223.00,411.00
223.00,411.00 223.00,429.00 223.00,429.00
223.00,429.00 138.00,429.00 138.00,429.00
138.00,429.00 138.00,119.00 138.00,119.00
138.00,119.00 156.00,119.00 156.00,119.00
156.00,119.00 156.00,101.00 156.00,101.00 Z
M 223.00,329.00
C 263.91,329.00 301.86,332.36 336.00,304.54
349.16,293.81 359.70,280.51 366.69,265.00
374.05,248.67 375.20,234.53 375.00,217.00
374.56,179.46 346.33,143.35 313.00,128.31
291.09,118.42 270.44,118.00 247.00,118.00
247.00,118.00 207.00,118.00 207.00,118.00
207.00,118.00 182.00,118.00 182.00,118.00
180.01,118.00 176.19,117.77 174.60,119.02
172.62,120.59 173.00,124.70 173.00,127.00
173.00,127.00 173.00,332.00 173.00,332.00
173.00,332.00 173.00,385.00 173.00,385.00
173.02,393.64 173.36,393.98 182.00,394.00
182.00,394.00 223.00,394.00 223.00,394.00
223.00,394.00 223.00,329.00 223.00,329.00 Z
M 223.00,166.00
C 223.00,166.00 266.00,166.00 266.00,166.00
306.49,166.06 336.49,205.40 321.39,244.00
319.11,249.82 315.95,255.30 311.82,260.00
300.28,273.13 284.59,280.92 267.00,281.00
267.00,281.00 253.00,281.00 253.00,281.00
253.00,281.00 223.00,281.00 223.00,281.00
223.00,281.00 223.00,166.00 223.00,166.00 Z
M 241.00,202.00
C 241.00,202.00 241.00,255.00 241.00,255.00
241.02,263.64 241.36,263.98 250.00,264.00
258.53,264.02 277.96,264.47 284.58,259.01
289.25,255.16 289.06,244.57 289.00,239.00
288.79,221.67 275.23,207.53 259.00,203.16
253.08,201.56 247.07,202.00 241.00,202.00 Z`
]
} as never;
export const Polywork: IconDefinition = {
prefix: "xxx",
iconName: "yyy",
icon: [
225, // SVG view box width
225, // SVG view box height
[],
"U+E002", // probably not important for SVG and JS approach
`M 153.00,155.00
C 153.00,177.49 155.16,201.18 134.00,215.78
117.90,226.88 101.50,224.00 83.00,224.00
83.00,224.00 57.00,224.00 57.00,224.00
36.75,224.00 21.13,223.75 8.22,205.00
5.78,201.46 3.67,197.12 2.44,193.00
0.64,186.98 0.01,182.25 0.00,176.00
0.00,176.00 0.00,49.00 0.00,49.00
0.05,19.44 19.43,0.05 49.00,0.00
49.00,0.00 176.00,0.00 176.00,0.00
203.99,0.04 223.96,18.67 224.00,47.00
224.00,47.00 224.00,89.00 224.00,89.00
224.00,112.86 226.74,132.44 204.00,147.95
200.27,150.49 196.33,152.29 192.00,153.56
182.70,156.30 170.69,156.05 161.00,156.00
157.55,155.98 156.36,155.95 153.00,155.00 Z
M 70.00,11.00
C 52.54,11.00 34.21,8.10 21.09,22.01
17.77,25.54 15.22,29.35 13.77,34.00
11.01,42.87 12.00,61.03 12.00,71.00
12.00,71.00 70.00,71.00 70.00,71.00
70.00,71.00 70.00,11.00 70.00,11.00 Z
M 141.00,71.00
C 141.00,71.00 141.00,19.00 141.00,19.00
140.94,10.87 139.73,11.02 132.00,11.00
132.00,11.00 82.94,11.00 82.94,11.00
82.94,11.00 82.94,65.00 82.94,65.00
82.65,71.56 84.51,70.99 91.00,71.00
91.00,71.00 141.00,71.00 141.00,71.00 Z
M 212.00,71.00
C 212.00,71.00 212.00,46.00 212.00,46.00
211.99,40.91 212.06,38.94 210.30,34.00
200.99,7.84 175.95,12.00 154.00,12.00
154.00,12.00 154.00,71.00 154.00,71.00
154.00,71.00 212.00,71.00 212.00,71.00 Z
M 70.00,84.00
C 70.00,84.00 11.00,84.00 11.00,84.00
11.00,84.00 11.00,136.00 11.00,136.00
11.02,144.64 11.36,144.98 20.00,145.00
20.00,145.00 70.00,145.00 70.00,145.00
70.00,145.00 70.00,84.00 70.00,84.00 Z
M 141.00,84.00
C 141.00,84.00 83.00,84.00 83.00,84.00
83.00,84.00 83.00,144.00 83.00,144.00
83.00,144.00 141.00,144.00 141.00,144.00
141.00,144.00 141.00,84.00 141.00,84.00 Z
M 212.00,84.00
C 212.00,84.00 154.00,84.00 154.00,84.00
154.00,84.00 154.00,145.00 154.00,145.00
176.30,145.00 201.22,148.17 210.45,122.00
212.02,117.55 211.99,114.59 212.00,110.00
212.00,110.00 212.00,84.00 212.00,84.00 Z
M 70.00,157.00
C 70.00,157.00 11.00,157.00 11.00,157.00
11.00,177.94 8.44,201.56 33.00,210.30
37.94,212.06 39.91,211.99 45.00,212.00
45.00,212.00 70.00,212.00 70.00,212.00
70.00,212.00 70.00,157.00 70.00,157.00 Z
M 141.00,158.00
C 141.00,158.00 83.00,158.00 83.00,158.00
83.00,158.00 83.00,213.00 83.00,213.00
83.00,213.00 106.00,213.00 106.00,213.00
110.59,212.99 113.55,213.02 118.00,211.45
143.85,202.34 141.00,180.01 141.00,158.00 Z`
]
} as never;
export const Fiverr: IconDefinition = {
prefix: "xxx",
iconName: "yyy",
icon: [
300,
300,
[],
"U+E002",
`M 131.00,0.21
C 131.00,0.21 161.00,0.21 161.00,0.21
175.05,0.02 192.91,4.74 206.00,9.81
219.21,14.93 234.03,23.67 245.00,32.61
271.56,54.26 290.61,84.25 297.40,118.00
298.88,125.38 299.99,130.37 300.00,138.00
300.00,138.00 300.00,164.00 300.00,164.00
299.71,188.69 285.28,221.51 270.63,241.00
257.74,258.14 242.56,270.96 224.00,281.57
207.02,291.29 180.65,299.97 161.00,300.00
161.00,300.00 137.00,300.00 137.00,300.00
126.65,299.88 110.91,295.97 101.00,292.67
56.73,277.91 22.19,243.07 7.08,199.00
-1.16,174.97 -0.04,162.44 0.00,138.00
0.02,124.75 3.92,109.38 8.58,97.00
29.12,42.42 74.15,9.01 131.00,0.21 Z
M 176.00,58.00
C 152.33,58.00 122.24,54.22 107.10,77.00
104.97,80.22 103.13,84.29 102.04,88.00
100.10,94.62 100.00,104.07 100.00,111.00
100.00,111.00 78.00,111.00 78.00,111.00
78.00,111.00 78.00,141.00 78.00,141.00
95.43,141.00 99.81,138.98 100.00,149.00
100.00,149.00 100.00,200.00 100.00,200.00
100.00,200.00 100.00,220.00 100.00,220.00
100.00,221.94 99.75,225.72 100.99,227.26
102.70,229.38 107.50,229.00 110.00,229.00
110.00,229.00 138.00,229.00 138.00,229.00
138.00,229.00 138.00,163.00 138.00,163.00
138.00,158.95 136.99,145.29 139.60,142.74
141.71,140.68 146.23,141.00 149.00,141.00
149.00,141.00 169.00,141.00 169.00,141.00
171.37,141.04 174.58,140.96 176.40,142.74
178.34,144.64 177.99,148.47 178.00,151.00
178.00,151.00 178.00,229.00 178.00,229.00
178.00,229.00 215.00,229.00 215.00,229.00
215.00,229.00 215.00,111.00 215.00,111.00
215.00,111.00 138.00,111.00 138.00,111.00
138.00,106.78 137.73,101.98 139.34,98.01
143.35,88.13 153.31,89.00 162.00,89.00
162.00,89.00 176.00,89.00 176.00,89.00
176.00,89.00 176.00,58.00 176.00,58.00 Z`
]
} as never;
export const JobTypes: {
[key in (typeof Employment)[number]["type"]]: IconDefinition;
} = {
hypothetical: faCircle,
volunteer: {
prefix: "xxx",
iconName: "yyy",
icon: [
1024,
1024,
[],
"U+E002",
`M 400.00,894.00
C 393.17,894.89 374.14,887.23 367.00,884.40
337.83,872.82 313.05,859.89 287.00,842.33
201.70,784.84 140.73,692.77 120.45,592.00
113.21,556.04 112.58,528.31 113.00,492.00
113.00,492.00 113.91,482.00 113.91,482.00
115.58,457.77 119.57,434.55 125.53,411.00
154.89,294.99 237.46,198.14 346.00,148.31
385.38,130.22 427.96,119.14 471.00,114.83
471.00,114.83 494.00,113.00 494.00,113.00
494.00,113.00 532.00,113.00 532.00,113.00
532.00,113.00 542.00,113.91 542.00,113.91
568.64,115.77 592.21,119.87 618.00,126.85
758.81,164.98 868.19,278.92 900.88,421.00
905.70,441.95 908.62,462.59 910.09,484.00
910.09,484.00 911.00,494.00 911.00,494.00
911.39,527.33 910.55,552.96 904.58,586.00
891.40,658.90 857.93,723.53 809.28,779.00
792.87,797.72 774.49,813.80 755.00,829.20
749.42,833.60 737.84,843.77 731.00,844.00
728.01,849.34 721.15,852.51 716.00,855.60
703.56,863.07 691.17,869.28 678.00,875.31
641.79,891.89 597.75,905.01 558.00,908.83
558.00,908.83 528.00,911.00 528.00,911.00
528.00,911.00 496.00,911.00 496.00,911.00
496.00,911.00 484.00,910.09 484.00,910.09
463.73,908.70 443.86,906.17 424.00,901.65
418.56,900.41 403.20,897.89 400.00,894.00 Z
M 745.00,714.00
C 745.00,714.00 757.63,698.00 757.63,698.00
766.05,686.76 775.00,673.35 781.69,661.00
809.23,610.20 820.67,559.53 820.00,502.00
820.00,502.00 819.09,492.00 819.09,492.00
818.06,477.07 816.33,462.67 813.21,448.00
787.94,329.09 694.66,234.49 575.00,210.55
560.13,207.58 547.09,205.95 532.00,204.91
532.00,204.91 522.00,204.00 522.00,204.00
469.75,203.39 418.76,213.14 372.00,237.26
358.36,244.29 341.24,254.73 329.00,263.89
329.00,263.89 310.00,279.00 310.00,279.00
312.13,285.23 315.03,286.64 319.42,291.16
319.42,291.16 341.72,313.42 341.72,313.42
341.72,313.42 412.84,384.58 412.84,384.58
412.84,384.58 435.00,406.84 435.00,406.84
437.17,408.96 442.60,414.77 445.00,415.72
449.00,417.29 452.75,413.42 456.00,411.46
461.69,408.04 463.60,408.01 470.00,408.00
470.00,408.00 553.00,408.00 553.00,408.00
555.59,408.00 558.53,407.87 561.00,408.70
565.25,410.11 577.20,418.95 581.00,422.08
596.88,435.17 612.92,450.77 625.58,467.00
649.77,498.03 667.75,532.22 675.55,571.00
677.70,581.69 679.02,596.10 679.00,607.00
678.99,613.69 677.78,624.35 676.58,631.00
676.05,633.92 673.99,641.67 674.40,644.00
675.17,648.33 687.28,659.10 691.07,662.84
691.07,662.84 726.72,698.28 726.72,698.28
731.59,703.16 738.54,711.74 745.00,714.00 Z
M 445.00,286.00
C 452.54,286.80 458.72,292.04 465.00,295.95
468.14,297.91 472.13,300.69 476.00,300.09
479.96,299.48 483.28,295.70 486.00,293.00
491.51,287.52 501.52,275.82 509.00,274.34
524.32,271.30 531.95,290.81 547.00,289.91
547.00,289.91 571.00,284.22 571.00,284.22
573.70,283.72 578.07,283.00 579.36,286.28
580.36,288.83 578.56,291.88 577.30,294.00
577.30,294.00 568.86,308.00 568.86,308.00
561.04,322.01 546.53,350.92 543.00,366.00
543.00,366.00 488.00,366.00 488.00,366.00
485.90,366.00 482.39,366.25 480.65,365.01
478.31,363.34 474.32,352.20 472.99,349.00
472.99,349.00 454.75,310.00 454.75,310.00
450.91,302.31 445.42,294.64 445.00,286.00 Z
M 416.00,444.00
C 416.00,444.00 381.92,410.07 381.92,410.07
381.92,410.07 314.17,342.58 314.17,342.58
314.17,342.58 290.00,318.00 290.00,318.00
287.86,315.87 283.32,310.70 280.09,311.03
277.98,311.24 276.27,313.52 275.00,315.02
275.00,315.02 265.42,327.00 265.42,327.00
252.81,343.25 241.25,363.31 232.69,382.00
193.14,468.36 194.64,564.63 236.26,650.00
243.09,664.02 251.58,677.29 260.58,690.00
300.75,746.77 357.46,787.24 424.00,807.28
455.72,816.83 488.99,820.38 522.00,820.00
522.00,820.00 532.00,819.09 532.00,819.09
547.91,818.00 563.40,816.04 579.00,812.58
585.72,811.08 602.88,805.91 608.00,806.00
608.00,806.00 622.00,799.80 622.00,799.80
622.00,799.80 649.00,787.89 649.00,787.89
649.00,787.89 686.00,766.28 686.00,766.28
686.00,766.28 714.00,745.00 714.00,745.00
711.59,737.94 702.66,731.07 697.28,725.72
697.28,725.72 669.92,698.07 669.92,698.07
665.14,693.37 658.44,687.61 655.00,682.00
649.32,683.67 638.29,694.47 632.00,698.64
620.59,706.22 604.21,712.80 591.00,716.57
578.14,720.24 549.20,724.94 536.00,725.00
536.00,725.00 517.00,726.00 517.00,726.00
517.00,726.00 487.00,725.00 487.00,725.00
487.00,725.00 461.00,722.27 461.00,722.27
418.48,716.14 374.27,698.72 355.31,657.00
347.38,639.54 344.97,619.97 345.00,601.00
345.06,560.19 364.22,515.43 386.51,482.00
391.39,474.68 397.42,466.67 403.13,460.00
407.31,455.12 413.08,449.45 416.00,444.00 Z
M 470.00,374.57
C 470.00,374.57 497.00,374.00 497.00,374.00
497.00,374.00 546.00,374.00 546.00,374.00
550.21,374.01 554.30,373.78 557.78,376.65
563.51,381.39 563.34,392.77 557.78,397.57
552.71,401.96 541.45,400.01 535.00,400.00
535.00,400.00 518.00,400.00 518.00,400.00
518.00,400.00 500.00,400.83 500.00,400.83
500.00,400.83 492.00,400.02 492.00,400.02
492.00,400.02 477.00,400.02 477.00,400.02
473.06,399.99 469.41,400.33 466.21,397.57
459.68,391.93 460.30,378.47 470.00,374.57 Z
M 459.00,429.00
C 459.00,429.00 458.00,429.00 458.00,429.00
458.00,429.00 459.00,430.00 459.00,430.00
459.00,430.00 459.00,429.00 459.00,429.00 Z
M 462.00,432.00
C 462.00,432.00 461.00,432.00 461.00,432.00
461.00,432.00 462.00,433.00 462.00,433.00
462.00,433.00 462.00,432.00 462.00,432.00 Z
M 464.00,434.00
C 464.00,434.00 463.00,434.00 463.00,434.00
463.00,434.00 464.00,435.00 464.00,435.00
464.00,435.00 464.00,434.00 464.00,434.00 Z
M 467.00,437.00
C 467.00,437.00 466.00,437.00 466.00,437.00
466.00,437.00 467.00,438.00 467.00,438.00
467.00,438.00 467.00,437.00 467.00,437.00 Z
M 470.00,440.00
C 470.00,440.00 469.00,440.00 469.00,440.00
469.00,440.00 470.00,441.00 470.00,441.00
470.00,441.00 470.00,440.00 470.00,440.00 Z
M 472.00,442.00
C 472.00,442.00 471.00,442.00 471.00,442.00
471.00,442.00 472.00,443.00 472.00,443.00
472.00,443.00 472.00,442.00 472.00,442.00 Z
M 475.00,445.00
C 475.00,445.00 474.00,445.00 474.00,445.00
474.00,445.00 475.00,446.00 475.00,446.00
475.00,446.00 475.00,445.00 475.00,445.00 Z
M 419.00,447.00
C 419.00,447.00 418.00,447.00 418.00,447.00
418.00,447.00 419.00,448.00 419.00,448.00
419.00,448.00 419.00,447.00 419.00,447.00 Z
M 478.00,448.00
C 478.00,448.00 477.00,448.00 477.00,448.00
477.00,448.00 478.00,449.00 478.00,449.00
478.00,449.00 478.00,448.00 478.00,448.00 Z
M 422.00,450.00
C 422.00,450.00 421.00,450.00 421.00,450.00
421.00,450.00 422.00,451.00 422.00,451.00
422.00,451.00 422.00,450.00 422.00,450.00 Z
M 480.00,450.00
C 480.00,450.00 479.00,450.00 479.00,450.00
479.00,450.00 480.00,451.00 480.00,451.00
480.00,451.00 480.00,450.00 480.00,450.00 Z
M 483.00,453.00
C 483.00,453.00 482.00,453.00 482.00,453.00
482.00,453.00 483.00,454.00 483.00,454.00
483.00,454.00 483.00,453.00 483.00,453.00 Z
M 427.00,455.00
C 427.00,455.00 426.00,455.00 426.00,455.00
426.00,455.00 427.00,456.00 427.00,456.00
427.00,456.00 427.00,455.00 427.00,455.00 Z
M 486.00,456.00
C 486.00,456.00 485.00,456.00 485.00,456.00
485.00,456.00 486.00,457.00 486.00,457.00
486.00,457.00 486.00,456.00 486.00,456.00 Z
M 430.00,458.00
C 430.00,458.00 429.00,458.00 429.00,458.00
429.00,458.00 430.00,459.00 430.00,459.00
430.00,459.00 430.00,458.00 430.00,458.00 Z
M 433.00,461.00
C 433.00,461.00 432.00,461.00 432.00,461.00
432.00,461.00 433.00,462.00 433.00,462.00
433.00,462.00 433.00,461.00 433.00,461.00 Z
M 491.00,461.00
C 491.00,461.00 490.00,461.00 490.00,461.00
490.00,461.00 491.00,462.00 491.00,462.00
491.00,462.00 491.00,461.00 491.00,461.00 Z
M 435.00,463.00
C 435.00,463.00 434.00,463.00 434.00,463.00
434.00,463.00 435.00,464.00 435.00,464.00
435.00,464.00 435.00,463.00 435.00,463.00 Z
M 567.00,520.00
C 566.65,504.24 556.88,488.77 543.00,481.32
535.67,477.39 528.16,476.54 526.10,475.49
520.81,472.81 524.40,467.66 521.98,464.60
519.97,462.05 505.67,463.00 502.00,463.00
502.00,463.00 502.85,473.00 502.85,473.00
502.85,473.00 518.00,489.96 518.00,489.96
518.00,489.96 551.00,523.00 551.00,523.00
551.00,523.00 567.00,520.00 567.00,520.00 Z
M 436.00,464.00
C 436.00,464.00 435.00,464.00 435.00,464.00
435.00,464.00 436.00,465.00 436.00,465.00
436.00,465.00 436.00,464.00 436.00,464.00 Z
M 494.00,464.00
C 494.00,464.00 493.00,464.00 493.00,464.00
493.00,464.00 494.00,465.00 494.00,465.00
494.00,465.00 494.00,464.00 494.00,464.00 Z
M 438.00,466.00
C 438.00,466.00 437.00,466.00 437.00,466.00
437.00,466.00 438.00,467.00 438.00,467.00
438.00,467.00 438.00,466.00 438.00,466.00 Z
M 496.00,466.00
C 496.00,466.00 495.00,466.00 495.00,466.00
495.00,466.00 496.00,467.00 496.00,467.00
496.00,467.00 496.00,466.00 496.00,466.00 Z
M 499.00,469.00
C 499.00,469.00 498.00,469.00 498.00,469.00
498.00,469.00 499.00,470.00 499.00,470.00
499.00,470.00 499.00,469.00 499.00,469.00 Z
M 443.00,471.00
C 443.00,471.00 442.00,471.00 442.00,471.00
442.00,471.00 443.00,472.00 443.00,472.00
443.00,472.00 443.00,471.00 443.00,471.00 Z
M 501.00,471.00
C 501.00,471.00 500.00,471.00 500.00,471.00
500.00,471.00 501.00,472.00 501.00,472.00
501.00,472.00 501.00,471.00 501.00,471.00 Z
M 446.00,474.00
C 446.00,474.00 445.00,474.00 445.00,474.00
445.00,474.00 446.00,475.00 446.00,475.00
446.00,475.00 446.00,474.00 446.00,474.00 Z
M 449.00,477.00
C 449.00,477.00 448.00,477.00 448.00,477.00
448.00,477.00 449.00,478.00 449.00,478.00
449.00,478.00 449.00,477.00 449.00,477.00 Z
M 451.00,479.00
C 451.00,479.00 450.00,479.00 450.00,479.00
450.00,479.00 451.00,480.00 451.00,480.00
451.00,480.00 451.00,479.00 451.00,479.00 Z
M 454.00,482.00
C 454.00,482.00 453.00,482.00 453.00,482.00
453.00,482.00 454.00,483.00 454.00,483.00
454.00,483.00 454.00,482.00 454.00,482.00 Z
M 459.00,487.00
C 459.00,487.00 458.00,487.00 458.00,487.00
458.00,487.00 459.00,488.00 459.00,488.00
459.00,488.00 459.00,487.00 459.00,487.00 Z
M 462.00,490.00
C 462.00,490.00 461.00,490.00 461.00,490.00
461.00,490.00 462.00,491.00 462.00,491.00
462.00,491.00 462.00,490.00 462.00,490.00 Z
M 466.00,494.00
C 466.00,494.00 465.00,494.00 465.00,494.00
465.00,494.00 466.00,495.00 466.00,495.00
466.00,495.00 466.00,494.00 466.00,494.00 Z
M 522.00,659.00
C 547.74,655.37 571.32,636.69 571.99,609.00
572.04,606.82 572.13,602.98 571.40,601.00
570.05,597.33 557.76,586.06 554.16,582.58
554.16,582.58 505.32,533.88 505.32,533.88
505.32,533.88 477.93,506.07 477.93,506.07
476.09,504.24 469.18,497.08 466.93,496.87
464.26,496.63 462.76,500.05 461.76,502.00
458.45,508.44 457.09,513.77 457.00,521.00
456.78,539.77 459.07,552.64 474.00,565.82
478.69,569.96 485.24,573.66 491.00,576.13
493.96,577.40 499.27,578.55 500.98,581.27
502.21,583.24 502.00,586.72 502.00,589.00
502.00,589.00 502.00,626.00 502.00,626.00
493.16,620.59 487.56,611.13 486.00,601.00
486.00,601.00 461.00,603.72 461.00,603.72
458.73,603.99 454.49,603.70 453.03,605.60
450.60,608.74 455.20,621.40 456.61,625.00
463.79,643.31 481.77,658.54 502.00,659.00
502.00,659.00 502.00,672.00 502.00,672.00
502.00,672.00 522.00,672.00 522.00,672.00
522.00,672.00 522.00,659.00 522.00,659.00 Z
M 558.00,528.00
C 558.00,528.00 557.00,528.00 557.00,528.00
557.00,528.00 558.00,529.00 558.00,529.00
558.00,529.00 558.00,528.00 558.00,528.00 Z
M 560.00,530.00
C 560.00,530.00 559.00,530.00 559.00,530.00
559.00,530.00 560.00,531.00 560.00,531.00
560.00,531.00 560.00,530.00 560.00,530.00 Z
M 563.00,533.00
C 563.00,533.00 562.00,533.00 562.00,533.00
562.00,533.00 563.00,534.00 563.00,534.00
563.00,534.00 563.00,533.00 563.00,533.00 Z
M 566.00,536.00
C 566.00,536.00 565.00,536.00 565.00,536.00
565.00,536.00 566.00,537.00 566.00,537.00
566.00,537.00 566.00,536.00 566.00,536.00 Z
M 571.00,541.00
C 571.00,541.00 570.00,541.00 570.00,541.00
570.00,541.00 571.00,542.00 571.00,542.00
571.00,542.00 571.00,541.00 571.00,541.00 Z
M 574.00,544.00
C 574.00,544.00 573.00,544.00 573.00,544.00
573.00,544.00 574.00,545.00 574.00,545.00
574.00,545.00 574.00,544.00 574.00,544.00 Z
M 576.00,546.00
C 576.00,546.00 575.00,546.00 575.00,546.00
575.00,546.00 576.00,547.00 576.00,547.00
576.00,547.00 576.00,546.00 576.00,546.00 Z
M 579.00,549.00
C 579.00,549.00 578.00,549.00 578.00,549.00
578.00,549.00 579.00,550.00 579.00,550.00
579.00,550.00 579.00,549.00 579.00,549.00 Z
M 582.00,552.00
C 582.00,552.00 581.00,552.00 581.00,552.00
581.00,552.00 582.00,553.00 582.00,553.00
582.00,553.00 582.00,552.00 582.00,552.00 Z
M 584.00,554.00
C 584.00,554.00 583.00,554.00 583.00,554.00
583.00,554.00 584.00,555.00 584.00,555.00
584.00,555.00 584.00,554.00 584.00,554.00 Z
M 587.00,557.00
C 587.00,557.00 586.00,557.00 586.00,557.00
586.00,557.00 587.00,558.00 587.00,558.00
587.00,558.00 587.00,557.00 587.00,557.00 Z
M 590.00,560.00
C 590.00,560.00 589.00,560.00 589.00,560.00
589.00,560.00 590.00,561.00 590.00,561.00
590.00,561.00 590.00,560.00 590.00,560.00 Z
M 592.00,562.00
C 592.00,562.00 591.00,562.00 591.00,562.00
591.00,562.00 592.00,563.00 592.00,563.00
592.00,563.00 592.00,562.00 592.00,562.00 Z
M 595.00,565.00
C 595.00,565.00 594.00,565.00 594.00,565.00
594.00,565.00 595.00,566.00 595.00,566.00
595.00,566.00 595.00,565.00 595.00,565.00 Z
M 598.00,568.00
C 598.00,568.00 597.00,568.00 597.00,568.00
597.00,568.00 598.00,569.00 598.00,569.00
598.00,569.00 598.00,568.00 598.00,568.00 Z
M 600.00,570.00
C 600.00,570.00 599.00,570.00 599.00,570.00
599.00,570.00 600.00,571.00 600.00,571.00
600.00,571.00 600.00,570.00 600.00,570.00 Z
M 603.00,573.00
C 603.00,573.00 602.00,573.00 602.00,573.00
602.00,573.00 603.00,574.00 603.00,574.00
603.00,574.00 603.00,573.00 603.00,573.00 Z
M 606.00,576.00
C 606.00,576.00 605.00,576.00 605.00,576.00
605.00,576.00 606.00,577.00 606.00,577.00
606.00,577.00 606.00,576.00 606.00,576.00 Z
M 608.00,578.00
C 608.00,578.00 607.00,578.00 607.00,578.00
607.00,578.00 608.00,579.00 608.00,579.00
608.00,579.00 608.00,578.00 608.00,578.00 Z
M 611.00,581.00
C 611.00,581.00 610.00,581.00 610.00,581.00
610.00,581.00 611.00,582.00 611.00,582.00
611.00,582.00 611.00,581.00 611.00,581.00 Z
M 614.00,584.00
C 614.00,584.00 613.00,584.00 613.00,584.00
613.00,584.00 614.00,585.00 614.00,585.00
614.00,585.00 614.00,584.00 614.00,584.00 Z
M 616.00,586.00
C 616.00,586.00 615.00,586.00 615.00,586.00
615.00,586.00 616.00,587.00 616.00,587.00
616.00,587.00 616.00,586.00 616.00,586.00 Z
M 619.00,589.00
C 619.00,589.00 618.00,589.00 618.00,589.00
618.00,589.00 619.00,590.00 619.00,590.00
619.00,590.00 619.00,589.00 619.00,589.00 Z
M 624.00,594.00
C 624.00,594.00 623.00,594.00 623.00,594.00
623.00,594.00 624.00,595.00 624.00,595.00
624.00,595.00 624.00,594.00 624.00,594.00 Z
M 627.00,597.00
C 627.00,597.00 626.00,597.00 626.00,597.00
626.00,597.00 627.00,598.00 627.00,598.00
627.00,598.00 627.00,597.00 627.00,597.00 Z
M 629.00,599.00
C 629.00,599.00 628.00,599.00 628.00,599.00
628.00,599.00 629.00,600.00 629.00,600.00
629.00,600.00 629.00,599.00 629.00,599.00 Z
M 630.00,600.00
C 630.00,600.00 629.00,600.00 629.00,600.00
629.00,600.00 630.00,601.00 630.00,601.00
630.00,601.00 630.00,600.00 630.00,600.00 Z
M 577.00,605.00
C 577.00,605.00 576.00,605.00 576.00,605.00
576.00,605.00 577.00,606.00 577.00,606.00
577.00,606.00 577.00,605.00 577.00,605.00 Z
M 635.00,605.00
C 635.00,605.00 634.00,605.00 634.00,605.00
634.00,605.00 635.00,606.00 635.00,606.00
635.00,606.00 635.00,605.00 635.00,605.00 Z
M 638.00,608.00
C 638.00,608.00 637.00,608.00 637.00,608.00
637.00,608.00 638.00,609.00 638.00,609.00
638.00,609.00 638.00,608.00 638.00,608.00 Z
M 639.00,609.00
C 639.00,609.00 638.00,609.00 638.00,609.00
638.00,609.00 639.00,610.00 639.00,610.00
639.00,610.00 639.00,609.00 639.00,609.00 Z
M 582.00,610.00
C 582.00,610.00 581.00,610.00 581.00,610.00
581.00,610.00 582.00,611.00 582.00,611.00
582.00,611.00 582.00,610.00 582.00,610.00 Z
M 640.00,610.00
C 640.00,610.00 639.00,610.00 639.00,610.00
639.00,610.00 640.00,611.00 640.00,611.00
640.00,611.00 640.00,610.00 640.00,610.00 Z
M 643.00,613.00
C 643.00,613.00 642.00,613.00 642.00,613.00
642.00,613.00 643.00,614.00 643.00,614.00
643.00,614.00 643.00,613.00 643.00,613.00 Z
M 587.00,615.00
C 587.00,615.00 586.00,615.00 586.00,615.00
586.00,615.00 587.00,616.00 587.00,616.00
587.00,616.00 587.00,615.00 587.00,615.00 Z
M 646.00,616.00
C 646.00,616.00 645.00,616.00 645.00,616.00
645.00,616.00 646.00,617.00 646.00,617.00
646.00,617.00 646.00,616.00 646.00,616.00 Z
M 590.00,618.00
C 590.00,618.00 589.00,618.00 589.00,618.00
589.00,618.00 590.00,619.00 590.00,619.00
590.00,619.00 590.00,618.00 590.00,618.00 Z
M 648.00,618.00
C 648.00,618.00 647.00,618.00 647.00,618.00
647.00,618.00 648.00,619.00 648.00,619.00
648.00,619.00 648.00,618.00 648.00,618.00 Z
M 593.00,621.00
C 593.00,621.00 592.00,621.00 592.00,621.00
592.00,621.00 593.00,622.00 593.00,622.00
593.00,622.00 593.00,621.00 593.00,621.00 Z
M 651.00,621.00
C 651.00,621.00 650.00,621.00 650.00,621.00
650.00,621.00 651.00,622.00 651.00,622.00
651.00,622.00 651.00,621.00 651.00,621.00 Z
M 595.00,623.00
C 595.00,623.00 594.00,623.00 594.00,623.00
594.00,623.00 595.00,624.00 595.00,624.00
595.00,624.00 595.00,623.00 595.00,623.00 Z
M 654.00,624.00
C 654.00,624.00 653.00,624.00 653.00,624.00
653.00,624.00 654.00,625.00 654.00,625.00
654.00,625.00 654.00,624.00 654.00,624.00 Z
M 598.00,626.00
C 598.00,626.00 597.00,626.00 597.00,626.00
597.00,626.00 598.00,627.00 598.00,627.00
598.00,627.00 598.00,626.00 598.00,626.00 Z
M 656.00,626.00
C 656.00,626.00 655.00,626.00 655.00,626.00
655.00,626.00 656.00,627.00 656.00,627.00
656.00,627.00 656.00,626.00 656.00,626.00 Z
M 601.00,629.00
C 601.00,629.00 600.00,629.00 600.00,629.00
600.00,629.00 601.00,630.00 601.00,630.00
601.00,630.00 601.00,629.00 601.00,629.00 Z
M 659.00,629.00
C 659.00,629.00 658.00,629.00 658.00,629.00
658.00,629.00 659.00,630.00 659.00,630.00
659.00,630.00 659.00,629.00 659.00,629.00 Z
M 603.00,631.00
C 603.00,631.00 602.00,631.00 602.00,631.00
602.00,631.00 603.00,632.00 603.00,632.00
603.00,632.00 603.00,631.00 603.00,631.00 Z
M 662.00,632.00
C 662.00,632.00 661.00,632.00 661.00,632.00
661.00,632.00 662.00,633.00 662.00,633.00
662.00,633.00 662.00,632.00 662.00,632.00 Z
M 606.00,634.00
C 606.00,634.00 605.00,634.00 605.00,634.00
605.00,634.00 606.00,635.00 606.00,635.00
606.00,635.00 606.00,634.00 606.00,634.00 Z
M 664.00,634.00
C 664.00,634.00 663.00,634.00 663.00,634.00
663.00,634.00 664.00,635.00 664.00,635.00
664.00,635.00 664.00,634.00 664.00,634.00 Z
M 609.00,637.00
C 609.00,637.00 608.00,637.00 608.00,637.00
608.00,637.00 609.00,638.00 609.00,638.00
609.00,638.00 609.00,637.00 609.00,637.00 Z
M 667.00,637.00
C 667.00,637.00 666.00,637.00 666.00,637.00
666.00,637.00 667.00,638.00 667.00,638.00
667.00,638.00 667.00,637.00 667.00,637.00 Z
M 611.00,639.00
C 611.00,639.00 610.00,639.00 610.00,639.00
610.00,639.00 611.00,640.00 611.00,640.00
611.00,640.00 611.00,639.00 611.00,639.00 Z
M 670.00,640.00
C 670.00,640.00 669.00,640.00 669.00,640.00
669.00,640.00 670.00,641.00 670.00,641.00
670.00,641.00 670.00,640.00 670.00,640.00 Z
M 614.00,642.00
C 614.00,642.00 613.00,642.00 613.00,642.00
613.00,642.00 614.00,643.00 614.00,643.00
614.00,643.00 614.00,642.00 614.00,642.00 Z
M 672.00,642.00
C 672.00,642.00 671.00,642.00 671.00,642.00
671.00,642.00 672.00,643.00 672.00,643.00
672.00,643.00 672.00,642.00 672.00,642.00 Z
M 619.00,647.00
C 619.00,647.00 618.00,647.00 618.00,647.00
618.00,647.00 619.00,648.00 619.00,648.00
619.00,648.00 619.00,647.00 619.00,647.00 Z
M 620.00,648.00
C 620.00,648.00 619.00,648.00 619.00,648.00
619.00,648.00 620.00,649.00 620.00,649.00
620.00,649.00 620.00,648.00 620.00,648.00 Z
M 622.00,650.00
C 622.00,650.00 621.00,650.00 621.00,650.00
621.00,650.00 622.00,651.00 622.00,651.00
622.00,651.00 622.00,650.00 622.00,650.00 Z
M 625.00,653.00
C 625.00,653.00 624.00,653.00 624.00,653.00
624.00,653.00 625.00,654.00 625.00,654.00
625.00,654.00 625.00,653.00 625.00,653.00 Z
M 627.00,655.00
C 627.00,655.00 626.00,655.00 626.00,655.00
626.00,655.00 627.00,656.00 627.00,656.00
627.00,656.00 627.00,655.00 627.00,655.00 Z
M 633.00,661.00
C 633.00,661.00 632.00,661.00 632.00,661.00
632.00,661.00 633.00,662.00 633.00,662.00
633.00,662.00 633.00,661.00 633.00,661.00 Z
M 635.00,663.00
C 635.00,663.00 634.00,663.00 634.00,663.00
634.00,663.00 635.00,664.00 635.00,664.00
635.00,664.00 635.00,663.00 635.00,663.00 Z
M 638.00,666.00
C 638.00,666.00 637.00,666.00 637.00,666.00
637.00,666.00 638.00,667.00 638.00,667.00
638.00,667.00 638.00,666.00 638.00,666.00 Z
M 641.00,669.00
C 641.00,669.00 640.00,669.00 640.00,669.00
640.00,669.00 641.00,670.00 641.00,670.00
641.00,670.00 641.00,669.00 641.00,669.00 Z
M 646.00,674.00
C 646.00,674.00 645.00,674.00 645.00,674.00
645.00,674.00 646.00,675.00 646.00,675.00
646.00,675.00 646.00,674.00 646.00,674.00 Z
M 649.00,677.00
C 649.00,677.00 648.00,677.00 648.00,677.00
648.00,677.00 649.00,678.00 649.00,678.00
649.00,678.00 649.00,677.00 649.00,677.00 Z
M 651.00,679.00
C 651.00,679.00 650.00,679.00 650.00,679.00
650.00,679.00 651.00,680.00 651.00,680.00
651.00,680.00 651.00,679.00 651.00,679.00 Z`
]
} as never,
fixed: faCalendar,
project: faListCheck
};
export const JobTypeDescriptions: {
[key in (typeof Employment)[number]["type"]]: string;
} = {
hypothetical: "",
volunteer: "Pro-bono work",
fixed: "Fixed-rate contract",
project: "Project-based contract"
};

View File

@ -1,21 +0,0 @@
export const Logos: {
link: string;
file: string;
alt: string;
}[] = [
{
link: "https://freecodecamp.org",
file: "fcc_primary_large.svg",
alt: "freeCodeCamp"
},
{
link: "https://rythm.fm",
file: "rythm.svg",
alt: "Rythm"
},
{
link: "https://deepgram.com",
file: "deepgram.svg",
alt: "Deepgram AI"
}
];

View File

@ -1,106 +0,0 @@
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import {
faDiscord,
faGithub,
faHashnode,
faLinkedinIn,
faMastodon,
faReddit,
faSlack,
faXTwitter
} from "@fortawesome/free-brands-svg-icons";
import { faHashtag } from "@fortawesome/free-solid-svg-icons";
import { Codeberg, Fiverr, Matrix, Peerlist, Polywork } from "./Icons";
export const Socials: {
icon: IconDefinition;
link: string;
label: string;
alt: string;
}[] = [
{
label: "Codeberg",
link: "https://codeberg.org/naomi-lgbt",
alt: "Codeberg Logo",
icon: Codeberg
},
{
label: "GitHub",
link: "https://github.com/nhcarrigan",
alt: "GitHub Logo",
icon: faGithub
},
{
label: "Discord",
link: "https://chat.naomi.lgbt",
alt: "Discord Logo",
icon: faDiscord
},
{
label: "Matrix",
link: "https://matrix.to/#/#naomi:matrix.org",
alt: "Element Logo",
icon: Matrix
},
{
label: "IRC",
link: "https://docs.nhcarrigan.com/#/contact?id=irc",
alt: "Hash symbol",
icon: faHashtag
},
{
label: "Slack",
link: "https://join.slack.com/t/naomi-lgbt/signup",
alt: "Slack Logo",
icon: faSlack
},
{
label: "Reddit",
link: "https://reddit.com/r/nhcarrigan",
alt: "Reddit Logo",
icon: faReddit
},
{
label: "Blog",
link: "https://blog.nhcarrigan.com",
alt: "Hashnode Logo",
icon: faHashnode
},
{
label: "LinkedIn",
link: "https://linkedin.com/in/naomi-lgbt",
alt: "LinkedIn Logo",
icon: faLinkedinIn
},
{
label: "Resume",
link: "https://resume.nhcarrigan.com",
alt: "Peerlist Logo",
icon: Peerlist
},
{
label: "Polywork",
link: "https://polywork.nhcarrigan.com/",
alt: "Polywork Logo",
icon: Polywork
},
{
label: "Fiverr",
link: "https://www.fiverr.com/nhcarrigan",
alt: "Fiverr Logo",
icon: Fiverr
},
{
label: "Mastodon",
link: "https://mastodon.social/@naomi_lgbt",
alt: "Mastodon Logo",
icon: faMastodon
},
{
label: "X (Twitter)",
link: "https://x.com/naomi_lgbt",
alt: "X Logo",
icon: faXTwitter
}
];

View File

@ -1,77 +0,0 @@
import { Certifications } from "./Certifications";
import { Employment } from "./Employment";
import { Testimonials } from "./Testimonials";
const getTimeBetween = (start: Date, end: Date) => {
const months =
(end.getFullYear() - start.getFullYear()) * 12 -
start.getMonth() +
end.getMonth();
return Math.floor(months / 12) > 0
? `${Math.floor(months / 12)} years, ${months % 12} months`
: `${months} months`;
};
export const Stats: {
title: string;
value: string | number;
}[] = [
{
title: "Current Positions Held",
value: Employment.filter((el) => !el.end).length
},
{
title: "Total Positions Held",
value: Employment.length
},
{
title: "Paid Positions Held",
value: Employment.filter(
(el) => el.type !== "hypothetical" && el.type !== "volunteer"
).length
},
{
title: "Pro-Bono Positons Held",
value: Employment.filter((el) => el.type === "volunteer").length
},
{
title: "Current Longest Position Held",
value: (() => {
const longest = Employment.filter(
(el) => !el.end && el.type !== "hypothetical"
).sort(
(a, b) =>
(b.end ?? new Date(Date.now())).getTime() -
b.start.getTime() -
((a.end ?? new Date(Date.now())).getTime() - a.start.getTime())
)[0];
return `${longest.title} - ${getTimeBetween(longest.start, longest.end ?? new Date(Date.now()))}`;
})()
},
{
title: "All-time Longest Position Held",
value: (() => {
const longest = Employment.filter(
(el) => el.type !== "hypothetical"
).sort(
(a, b) =>
(b.end ?? new Date(Date.now())).getTime() -
b.start.getTime() -
((a.end ?? new Date(Date.now())).getTime() - a.start.getTime())
)[0];
return `${longest.title} - ${getTimeBetween(longest.start, longest.end ?? new Date(Date.now()))}`;
})()
},
{
title: "Clients Served",
value: new Set(Employment.map((el) => el.company)).size
},
{
title: "Reviews Received",
value: Testimonials.length
},
{
title: "Certifications Earned",
value: Certifications.length
}
];

51
src/app/contact/page.tsx Normal file
View File

@ -0,0 +1,51 @@
import { Rule } from "@/components/rule";
import { Social } from "@/components/social";
import { Donate, HireMe, Socials } from "@/config/Socials";
const Contact = (): JSX.Element => {
return (
<>
<main className="w-4/5 text-center max-w-4xl m-auto mt-16 mb-16 rounded-lg">
<h1 className="text-5xl">Contact Us</h1>
<p>
We offer many different ways to get in touch with us. Use the buttons
below to choose your preferred method.
</p>
<p>Note that the items are sorted alphabetically.</p>
<Rule />
<section>
<div>
{[HireMe, Donate].map(
({ link, label, alt, icon, color, background }) => (
<Social
key={label}
link={link}
label={label}
alt={alt}
icon={icon}
color={color}
background={background}
/>
),
)}
{Socials.sort((a, b) => a.label.localeCompare(b.label)).map(
({ link, label, alt, icon, color, background }) => (
<Social
key={label}
link={link}
label={label}
alt={alt}
icon={icon}
color={color}
background={background}
/>
),
)}
</div>
</section>
</main>
</>
);
};
export default Contact;

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

56
src/app/globals.css Normal file
View File

@ -0,0 +1,56 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: "JetBrains Mono";
src: url(../assets/JetBrainsMono-Regular.ttf);
}
:root {
--foreground: #04624f;
--background: #abfcecdd;
--current: #00a86b;
--former: #ff69b4;
}
.dark {
--foreground: #abfcec;
--background: #04624fdd;
--current: rgb(140, 255, 152);
--former: rgb(255, 148, 253);
}
body::before {
background: url(https://cdn.nhcarrigan.com/background.png);
background-size: cover;
background-position: center;
width: 100%;
height: 100%;
z-index: -1;
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 1;
pointer-events: none;
}
main,
nav {
color: var(--foreground);
background-color: var(--background);
font-family: "JetBrains Mono", monospace;
}
a {
color: unset;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}

View File

@ -1,50 +0,0 @@
hr {
border: 2px solid var(--text);
margin: 1rem auto;
max-width: 1125px;
}
h2 {
font-size: 1.5rem;
}
section {
margin-bottom: 1rem;
}
button {
border: 2px solid var(--text);
background-color: var(--bg);
color: var(--text);
font-size: 1.5rem;
cursor: pointer;
}
button:disabled {
background-color: var(--text);
color: var(--bg);
cursor: not-allowed;
}
.buttons {
width: 100%;
max-width: 1200px;
display: flex;
justify-content: space-evenly;
margin: auto;
flex-wrap: wrap;
}
.smaller {
font-size: 1rem;
max-width: 1125px;
margin: auto;
}
.logo-container {
margin: auto;
max-width: 1125px;
display: flex;
justify-content: space-around;
flex-wrap: wrap;
}

View File

@ -1,48 +0,0 @@
<h1>Naomi Carrigan</h1>
<p>Software Engineer | Community Manager</p>
<p class="smaller">
Creative professional with a focus on developing unique Discord bots and
fostering engaged online communities. Eager to leverage technical and
interpersonal skills to drive impactful, user-centric projects. Seeking
clients where my expertise in designing user-centric solutions and managing
vibrant digital spaces can drive engagement and growth.
</p>
<hr />
<section>
<h2>Contact</h2>
<app-social *ngFor="let social of socials" [social]="social" />
</section>
<hr />
<section>
<h2>Featured Clients</h2>
<div class="logo-container">
<app-logo *ngFor="let logo of logos" [logo]="logo" />
</div>
</section>
<hr />
<div class="buttons">
<button (click)="changeView('resume')" [disabled]="view === 'resume'">
Resume
</button>
<button
(click)="changeView('testimonials')"
[disabled]="view === 'testimonials'"
>
Testimonials
</button>
<button
(click)="changeView('certifications')"
[disabled]="view === 'certifications'"
>
Certifications
</button>
<button (click)="changeView('stats')" [disabled]="view === 'stats'">
Stats
</button>
</div>
<section>
<app-timeline *ngIf="view === 'resume'"></app-timeline>
<app-testimonials *ngIf="view === 'testimonials'"></app-testimonials>
<app-certifications *ngIf="view === 'certifications'"></app-certifications>
<app-stats *ngIf="view === 'stats'"></app-stats>
</section>

View File

@ -1,56 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Logos } from "../config/Logos";
import { Socials } from "../config/Socials";
import { HomeComponent } from "./home.component";
describe("HomeComponent", () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
let compiled: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomeComponent]
}).compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.nativeElement;
});
it("should create", () => {
expect(component).toBeTruthy();
});
it("should render correctly", () => {
const header = compiled.querySelector("h1");
expect(header?.innerText.trim()).toBe("Naomi Carrigan");
const socials = compiled.querySelectorAll("app-social");
expect(socials).toHaveSize(Socials.length);
const logos = compiled.querySelectorAll("app-logo");
expect(logos).toHaveSize(Logos.length);
});
it("should render the timeline view", () => {
component.changeView("resume");
fixture.detectChanges();
compiled = fixture.nativeElement;
const timeline = compiled.querySelector("app-timeline");
expect(timeline).toBeDefined();
const testimonials = compiled.querySelector("app-testimonials");
expect(testimonials).toBeNull();
});
it("should render the testimonials view", () => {
component.changeView("testimonials");
fixture.detectChanges();
compiled = fixture.nativeElement;
const timeline = compiled.querySelector("app-timeline");
expect(timeline).toBeNull();
const testimonials = compiled.querySelector("app-testimonials");
expect(testimonials).toBeDefined();
});
});

View File

@ -1,45 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { CertificationsComponent } from "../certifications/certifications.component";
import { Logos } from "../config/Logos";
import { Socials } from "../config/Socials";
import { JobComponent } from "../job/job.component";
import { LogoComponent } from "../logo/logo.component";
import { SocialComponent } from "../social/social.component";
import { StatsComponent } from "../stats/stats.component";
import { TestimonialsComponent } from "../testimonials/testimonials.component";
import { TimelineComponent } from "../timeline/timeline.component";
/**
*
*/
@Component({
selector: "app-home",
standalone: true,
imports: [
CommonModule,
SocialComponent,
JobComponent,
LogoComponent,
TimelineComponent,
TestimonialsComponent,
CertificationsComponent,
StatsComponent
],
templateUrl: "./home.component.html",
styleUrl: "./home.component.css"
})
export class HomeComponent {
public socials = Socials;
public logos = Logos;
public view: "resume" | "testimonials" | "certifications" | "stats" =
"resume";
/**
* @param {string} view The view to load.
*/
changeView(view: typeof this.view) {
this.view = view;
}
}

View File

@ -1,120 +0,0 @@
h2 {
font-size: 2rem;
}
p {
font-size: 1.5rem;
}
article p {
font-size: 1.25rem;
}
button {
cursor: pointer;
}
hr {
border: 1px solid var(--text);
}
.job {
padding: 10px 40px;
position: relative;
width: 50%;
background-color: transparent;
color: var(--bg);
display: block;
border: none;
}
.info {
color: var(--text);
border-radius: 1rem;
border: 2px solid var(--text);
overflow: hidden;
position: relative;
}
.icon {
position: relative;
top: -5rem;
z-index: 1;
background-color: var(--bg);
padding: 0;
border-radius: 50%;
}
.left {
left: 0;
}
.right {
left: 50%;
}
.right::after {
left: -12.5px;
}
.hypothetical .info,
.hypothetical .info hr,
.hypothetical a,
.hypothetical a:visited {
color: #abfcec;
border-color: #abfcec;
border-style: dotted;
}
.hypothetical.icon {
color: #abfcec;
}
.current .info,
.current .info hr,
.current a,
.current a:visited,
.current.icon {
color: #8cff98;
border-color: #8cff98;
}
.former .info,
.former .info hr,
.former a,
.former a:visited,
.former.icon {
color: #ff94fd;
border-color: #ff94fd;
}
.date,
.desc {
font-style: italic;
}
.logo {
max-height: 100px;
border-radius: 50%;
}
.icon {
font-size: 2rem;
}
@media screen and (max-width: 600px) {
.job {
width: 100%;
padding-left: 70px;
padding-right: 25px;
}
.right {
left: 0;
}
.left::after,
.right::after {
left: 17.5px;
}
.icon {
left: calc(37.5px - 50vw);
}
}

View File

@ -1,27 +0,0 @@
<button class="job {{ align }} {{ type }}" (click)="expand()">
<div class="info">
<h2>{{ job.title }}</h2>
<img
*ngIf="job.logo && expanded"
class="logo"
[src]="'/assets/img/icons/' + job.logo"
[alt]="job.company + ' Logo'"
/>
<p>{{ job.company }}</p>
<p class="date">
{{ date }}
</p>
<p class="desc" *ngIf="icon && expanded">{{ iconDescription }}</p>
<hr *ngIf="expanded" />
<article *ngIf="expanded">
<p *ngFor="let line of description">{{ line }}</p>
</article>
</div>
</button>
<fa-icon
*ngIf="icon"
class="icon {{ type }}"
[icon]="icon"
[attr.alt]="iconDescription"
/>

View File

@ -1,216 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Employment } from "../config/Employment";
import { JobTypeDescriptions, JobTypes } from "../config/Icons";
import { JobComponent } from "./job.component";
describe("JobComponent", () => {
let component: JobComponent;
let fixture: ComponentFixture<JobComponent>;
let compiled: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [JobComponent]
}).compileComponents();
fixture = TestBed.createComponent(JobComponent);
component = fixture.componentInstance;
compiled = fixture.nativeElement;
});
for (const expected of Employment) {
it("should parse properties correctly", () => {
component.job = expected;
component.align = "left";
fixture.detectChanges();
expect(component.job).toBe(expected);
expect(component.align).toBe("left");
expect(component.type).toBe(
expected.type === "hypothetical"
? "hypothetical"
: expected.end
? "former"
: "current"
);
expect(component.icon).toBe(JobTypes[expected.type]);
expect(component.iconDescription).toBe(
JobTypeDescriptions[expected.type]
);
expect(component.expanded).toBeFalse();
expect(component.description).toEqual(expected.description.split(/\n+/));
expect(component.date).toBe(
expected.end
? `${expected.start.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
})} - ${expected.end.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
})}`
: expected.start.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
})
);
});
it("should render a job correctly", () => {
component.job = expected;
component.ngOnInit();
fixture.detectChanges();
compiled = fixture.nativeElement;
/* Button wrapper */
const button = compiled.querySelector("button");
expect(button?.classList).toContain("left");
expect(button?.classList).toContain(
expected.type === "hypothetical"
? "hypothetical"
: expected.end
? "former"
: "current"
);
/* Timeline Icon */
const icon = compiled.querySelector("fa-icon");
expect(icon?.classList).toContain("icon");
expect(icon?.classList).toContain(
expected.type === "hypothetical"
? "hypothetical"
: expected.end
? "former"
: "current"
);
const svg = icon?.querySelector("svg");
const path = svg?.querySelector("path");
expect(path?.getAttribute("d")).toBe(
JobTypes[expected.type].icon[4] as string
);
/* Rendered content from job object */
const title = compiled.querySelector("h2");
expect(title?.innerText.trim()).toBe(expected.title);
const classList = compiled.querySelector("button")?.classList;
expect(classList).toContain(
expected.type === "hypothetical"
? "hypothetical"
: expected.end
? "former"
: "current"
);
const date = compiled.querySelector(".date");
expect(date?.textContent?.trim()).toBe(
expected.end
? `${expected.start.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
})} - ${expected.end.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
})}`
: expected.start.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
})
);
/* Test the stuff that's hidden */
const article = compiled.querySelector("article");
expect(article).toBeNull();
const logo = compiled.querySelector(".logo");
expect(logo).toBeNull();
const iconDescription = compiled.querySelector(".desc");
expect(iconDescription).toBeNull();
});
it("should expand a job correctly", () => {
component.job = expected;
component.ngOnInit();
if (!component.expanded) {
component.expand();
}
fixture.detectChanges();
compiled = fixture.nativeElement;
expect(component.expanded).toBeTrue();
/* Button wrapper */
const button = compiled.querySelector("button");
expect(button?.classList).toContain("left");
expect(button?.classList).toContain(
expected.type === "hypothetical"
? "hypothetical"
: expected.end
? "former"
: "current"
);
/* Timeline Icon */
const icon = compiled.querySelector("fa-icon");
expect(icon?.classList).toContain("icon");
expect(icon?.classList).toContain(
expected.type === "hypothetical"
? "hypothetical"
: expected.end
? "former"
: "current"
);
const svg = icon?.querySelector("svg");
const path = svg?.querySelector("path");
expect(path?.getAttribute("d")).toBe(
JobTypes[expected.type].icon[4] as string
);
/* Rendered content from job object should NOT be changed */
const title = compiled.querySelector("h2");
expect(title?.innerText.trim()).toBe(expected.title);
const classList = compiled.querySelector("button")?.classList;
expect(classList).toContain(
expected.type === "hypothetical"
? "hypothetical"
: expected.end
? "former"
: "current"
);
const date = compiled.querySelector(".date");
expect(date?.textContent?.trim()).toBe(
expected.end
? `${expected.start.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
})} - ${expected.end.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
})}`
: expected.start.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
})
);
/* Hidden stuff should now be visible */
const article = compiled.querySelector("article");
expect(article?.innerText.replace(/\n+/g, "\n")).toBe(
expected.description
.split(/\n+/)
.map((el) => el.trim())
.join("\n")
);
if (expected.logo) {
const logo = compiled.querySelector(".logo");
expect(logo?.getAttribute("src")).toBe(
`/assets/img/icons/${expected.logo}`
);
expect(logo?.classList).toContain("logo");
}
const iconDescription = compiled.querySelector(".desc");
expect(iconDescription?.textContent?.trim()).toBe(
JobTypeDescriptions[expected.type]
);
});
}
});

View File

@ -1,58 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { Employment } from "../config/Employment";
import { JobTypeDescriptions, JobTypes } from "../config/Icons";
/**
*
*/
@Component({
selector: "app-job",
standalone: true,
imports: [CommonModule, FontAwesomeModule],
templateUrl: "./job.component.html",
styleUrl: "./job.component.css"
})
export class JobComponent implements OnInit {
@Input() job: (typeof Employment)[number] = {} as never;
@Input() align: "left" | "right" = "left";
public type!: "current" | "former" | (typeof Employment)[number]["type"];
public icon!: IconDefinition;
public iconDescription = "";
public expanded = false;
public description: string[] = [];
public date = "";
/**
*
*/
ngOnInit() {
this.description = this.job.description.split(/\n+/);
this.icon = JobTypes[this.job.type];
this.iconDescription = JobTypeDescriptions[this.job.type];
if (this.job.end) {
this.type = "former";
this.date = `${this.job.start.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
})} - ${this.job.end.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
})}`;
return;
}
this.type = this.job.type === "hypothetical" ? "hypothetical" : "current";
this.date = this.job.start.toLocaleDateString("en-GB", {
month: "long",
year: "numeric"
});
}
/** */
expand() {
this.expanded = !this.expanded;
}
}

36
src/app/layout.tsx Normal file
View File

@ -0,0 +1,36 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ClientNavigation } from "@/components/navigation";
import Script from "next/script";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
{children}
<header>
<ClientNavigation />
</header>
</body>
<Script
type="text/javascript"
id="hs-script-loader"
async
defer
src="//js.hs-scripts.com/47086586.js"
></Script>
</html>
);
}

View File

@ -1,3 +0,0 @@
a {
height: 50px;
}

View File

@ -1,3 +0,0 @@
<a href="{{ link }}" target="_blank" rel="noopener noreferrer">
<img class="logo" src="{{ file }}" alt="{{ alt }}" height="50px" />
</a>

View File

@ -1,47 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Logos } from "../config/Logos";
import { LogoComponent } from "./logo.component";
describe("LogoComponent", () => {
let component: LogoComponent;
let fixture: ComponentFixture<LogoComponent>;
let compiled: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LogoComponent]
}).compileComponents();
fixture = TestBed.createComponent(LogoComponent);
component = fixture.componentInstance;
});
for (const expected of Logos) {
it(`should parse ${expected.alt} properties correctly`, () => {
component.logo = expected;
fixture.detectChanges();
expect(component.logo).toBe(expected);
expect(component.link).toBe(expected.link);
expect(component.alt).toBe(expected.alt);
expect(component.file).toBe(`/assets/img/${expected.file}`);
});
it(`should render ${expected.alt} correctly`, () => {
component.logo = expected;
fixture.detectChanges();
compiled = fixture.nativeElement;
const link = compiled.querySelector("a");
expect(link?.href).toContain(expected.link);
expect(link?.target).toBe("_blank");
expect(link?.rel).toBe("noopener noreferrer");
const logo = link?.querySelector("img");
expect(logo?.src).toContain(`/assets/img/${expected.file}`);
expect(logo?.classList).toContain("logo");
expect(logo?.alt).toBe(expected.alt);
expect(logo?.height).toBe(50);
});
}
});

View File

@ -1,29 +0,0 @@
import { Component, Input, OnInit } from "@angular/core";
import { Logos } from "../config/Logos";
/**
*
*/
@Component({
selector: "app-logo",
standalone: true,
imports: [],
templateUrl: "./logo.component.html",
styleUrl: "./logo.component.css"
})
export class LogoComponent implements OnInit {
@Input() logo: (typeof Logos)[number] = {} as never;
public link = "";
public alt = "";
public file = "";
/**
*
*/
ngOnInit(): void {
this.link = this.logo.link;
this.alt = this.logo.alt;
this.file = `/assets/img/${this.logo.file}`;
}
}

135
src/app/manual/page.tsx Normal file
View File

@ -0,0 +1,135 @@
import { Rule } from "@/components/rule";
import Image from "next/image";
const Manual = (): JSX.Element => {
return (
<>
<main className="w-4/5 text-center max-w-4xl m-auto mt-16 mb-16 rounded-lg">
<h1 className="text-5xl">Naomi&apos;s User Manual</h1>
<p className="text-3xl">Model T42-P9000</p>
<section>
<p className="mb-2">
Hiya! This page is meant to give you a run-down of what working with
me is like. It&apos;s like a li&apos;l insight into the way I best
interact with people and perform on a team.
</p>
<Image
className="m-auto"
src="https://cdn.nhcarrigan.com/hire.jpeg"
alt="If you knew I was so unstable, why'd you hire me?"
width={500}
height={500}
/>
<Rule />
</section>
<section>
<h2 className="text-3xl">My Values</h2>
<p className="mb-2">
I tend to be very firm in my values, to the degree that I shy away
from organisations or projects which do not align with my ethics.
</p>
<ul className="w-4/5 m-auto">
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
First and foremost, do not pass judgement. I will not begrudge you
for your religious beliefs, political alignments, or any other
aspects of your life. And I expect the same courtesy in return.
You do not have to accept who I am, or support my choices, but we
need to maintain a respectful and cordial professional
relationship. Expressing hateful, mean-spirited, or vitriolic
comments does not foster such an environment, and my tolerance for
such is nil.
</li>
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
Second, we shall do no harm. I very strongly believe that
technology is meant to advance our society, not regress it.
Software should not be used to automate people&apos;s careers
away, or replace creative works with pale imitations, or
harass/target someone, or restrict access to information.
</li>
</ul>
<Rule />
</section>
<section>
<h2 className="text-3xl">Environment</h2>
<p className="mb-2">
I have found that I best thrive and produce the greatest works in
specific environments.
</p>
<ul className="w-4/5 m-auto">
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
Ideas should be freely discussed, challenged, and evaluated. I am
not a &quot;yes girl&quot; to blindly follow orders. If I hear a
plan that I think misses the mark, I will absolutely voice my
concerns. And I welcome the same scrutiny in return. Discussion
needs to be constructive and fruitful - if we&apos;re going around
in circles, it&apos;s time to table it and sleep on our thoughts.
</li>
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
We are not our ideas, however. &quot;This is bad and you should
feel bad&quot; helps no one. &quot;I&apos;m not sure that is the
best approach, can we consider doing this instead?&quot; keeps the
conversation focused on the right things and moving forward.
</li>
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
ADHD means my productivity is highly fluctuating. There are days
where I can sit and bang out a week of work in 16 hours, and there
are days where I stare mindlessly at the screen and then realise
the sun has set already. Flexibility in both schedule and
deadlines is very important to me.
</li>
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
Likewise, insomnia can rear its ugly head and absolutely wreck my
sleep schedule without any warning. Asynchronous comms are always
preferred over meetings, but if a meeting must happen the
afternoon hours are the best. Too early and I&apos;ll likely fail
to wake up. Too late and I might be incoherent.
</li>
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
If given something like a Trello or a Monday board, I will 100%
make it pretty and load every single task ever on there. I&apos;m
a sucker for organising workloads and the easiest way for me to be
transparent about the work I&apos;m doing is to just let you look
at a task board. If you say &quot;what did you do yesterday&quot;
I will not remember this for I have slept since then.
</li>
</ul>
<Rule />
</section>
<section>
<h2 className="text-3xl">Communication</h2>
<p>
As with many people, there are certain approaches to communication
that are more effective for me.
</p>
<ul className="w-4/5 m-auto">
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
Don&apos;t just say &quot;hello&quot; in my DMs or ping me without
context. You&apos;ll pull me out of a workflow to wait for the
rest of the message. Send it all at once, or save the ping for the
last part.
</li>
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
I have generalised anxiety disorder. Please for the love of all
things, do NOT say &quot;we need to talk&quot; or &quot;do you
have time to meet&quot;. I will 100% sit there right up until the
meeting starts stressing about getting fired and not actually
getting any work done. If you need to call me out, just rip the
bandage off and come out of the gate with it.
</li>
<li className="mb-2 relative before:content-['🩵'] before:absolute before:left-[-1em]">
I dunno if you noticed the tone changed in this document about a
million times. That&apos;s pretty much Naomi in a nutshell.
I&apos;ll go from collected and eloquent to manic and sending
messages faster than Discord can process. I try my hardest to stay
professional, but ADHD makes that very hard. I&apos;ll swear
sometimes, I&apos;ll say totally off-the-wall things. But I really
do try!
</li>
</ul>
</section>
</main>
</>
);
};
export default Manual;

55
src/app/page.tsx Normal file
View File

@ -0,0 +1,55 @@
"use client";
import { NavItems } from "@/config/NavItems";
import Image from "next/image";
import React, { useEffect, useState } from "react";
export default function Home() {
const [isDarkMode, setIsDarkMode] = useState(false);
useEffect(() => {
const savedTheme = localStorage.getItem("theme");
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
const isDark = savedTheme === "dark" || (!savedTheme && prefersDark);
document.documentElement.classList.toggle("dark", isDark);
setIsDarkMode(isDark);
}, []);
return (
<div>
<div className="absolute top-0 right-[33vw] z-100 w-[100vh] h-[100vh] rotate-90 hidden lg:block">
<svg viewBox="0 0 500 500" className="absolute">
<path
d="M0,100 C150,200 350,0 500,100 L500,00 L0,0 Z"
style={{ stroke: "none", fill: "var(--foreground)" }}
></path>
</svg>
</div>
<main className="grid grid-cols-[2fr,1fr] min-h-screen">
<section className="bg-[--background] text-[--foreground] flex flex-col items-left justify-center text-left pl-5">
<h1 className="text-4xl mb-2">Naomi Carrigan</h1>
<Image
src="https://cdn.nhcarrigan.com/profile.png"
alt="Naomi Carrigan"
width={200}
height={200}
className="rounded-full"
/>
<p className="text-3xl">Software Engineer</p>
<p className="text-3xl">Community Manager</p>
</section>
<section className="bg-[--foreground] text-[--background] flex flex-col items-center justify-center">
{NavItems.map((item, index) => (
<a
key={item.href}
href={item.href}
className="block py-2 px-4 text-2xl hover:bg-[--background] hover:text-[--foreground]"
>
{index % 2 ? "🩷" : "🩵"} {item.text}
</a>
))}
</section>
</main>
</div>
);
}

View File

@ -1,73 +0,0 @@
article {
border: 2px solid var(--text);
margin: 10px;
padding: 10px;
position: relative;
border-radius: 10px;
}
article::after {
content: "";
position: absolute;
border-style: solid;
border-width: 15px 0 15px 15px;
border-color: transparent transparent transparent var(--text);
top: 50%;
right: -15px;
transform: translateY(-50%);
}
.green {
border-color: #8cff98;
color: #8cff98;
}
.green article {
border-color: #8cff98;
}
.green a {
color: #8cff98;
}
.green article::after {
border-color: transparent transparent transparent #8cff98;
}
.review {
margin: auto;
max-width: 1200px;
display: grid;
grid-template-columns: 4fr 1fr;
align-items: center;
}
.attrib p,
.attrib {
font-size: 1.25rem;
text-decoration: none;
}
@media screen and (max-width: 600px) {
article::after {
content: "";
position: absolute;
border-style: solid;
border-width: 15px 15px 0 15px;
border-color: var(--text) transparent transparent transparent;
bottom: -15px;
left: 50%;
right: auto;
top: auto;
transform: translateX(-50%);
}
.green article::after {
border-color: #8cff98 transparent transparent transparent;
}
.review {
grid-template-columns: 100%;
grid-template-rows: auto auto;
}
}

View File

@ -1,11 +0,0 @@
<div class="{{ class }}">
<article>{{ content }}</article>
<a
href="{{ sourceUrl }}"
target="_blank"
rel="noopener noreferrer"
class="attrib"
>
<p><fa-icon [icon]="sourceIcon"></fa-icon> {{ name }}</p>
</a>
</div>

View File

@ -1,53 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Testimonials } from "../config/Testimonials";
import { ReviewComponent } from "./review.component";
describe("ReviewComponent", () => {
let component: ReviewComponent;
let fixture: ComponentFixture<ReviewComponent>;
let compiled: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ReviewComponent]
}).compileComponents();
fixture = TestBed.createComponent(ReviewComponent);
component = fixture.componentInstance;
});
for (const expected of Testimonials) {
it(`should parse ${expected.name} properties correctly`, () => {
component.review = expected;
fixture.detectChanges();
expect(component.review).toBe(expected);
expect(component.name).toBe(expected.name);
expect(component.sourceIcon).toEqual(expected.sourceIcon);
expect(component.sourceUrl).toContain(expected.sourceUrl);
expect(component.content).toBe(expected.content);
});
it(`should render ${expected.name} correctly`, () => {
component.review = expected;
fixture.detectChanges();
compiled = fixture.nativeElement;
const link = compiled.querySelector("a");
expect(link?.href).toContain(expected.sourceUrl);
expect(link?.target).toBe("_blank");
expect(link?.rel).toBe("noopener noreferrer");
const content = compiled.querySelector("article");
expect(content?.textContent?.trim()).toBe(expected.content);
const credit = compiled.querySelector("p");
expect(credit?.textContent?.trim()).toBe(expected.name);
const icon = compiled.querySelector("fa-icon");
const svg = icon?.querySelector("svg");
const path = svg?.querySelector("path");
expect(path?.getAttribute("d")).toBe(
expected.sourceIcon.icon[4] as string
);
});
}
});

View File

@ -1,36 +0,0 @@
import { Component, Input, OnInit } from "@angular/core";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { Testimonials } from "../config/Testimonials";
/**
*
*/
@Component({
selector: "app-review",
standalone: true,
imports: [FontAwesomeModule],
templateUrl: "./review.component.html",
styleUrl: "./review.component.css"
})
export class ReviewComponent implements OnInit {
@Input() review: (typeof Testimonials)[number] = {} as never;
@Input() even = false;
public name = "";
public content = "";
public sourceIcon: IconDefinition = {} as never;
public sourceUrl = "";
public class = "review";
/**
*
*/
ngOnInit(): void {
this.name = this.review.name;
this.content = this.review.content;
this.sourceIcon = this.review.sourceIcon;
this.sourceUrl = this.review.sourceUrl;
this.class = this.even ? "review green" : "review";
}
}

40
src/app/reviews/page.tsx Normal file
View File

@ -0,0 +1,40 @@
import { Certification } from "@/components/cert";
import { Review } from "@/components/review";
import { Rule } from "@/components/rule";
import { Certifications } from "@/config/Certifications";
import { Testimonials } from "@/config/Testimonials";
import { Charm } from "next/font/google";
const Reviews = (): JSX.Element => {
return (
<>
<main className="w-[95%] text-center max-w-4xl m-auto mt-16 mb-16 rounded-lg">
<h1 className="text-5xl">Client Reviews</h1>
<section>
<p className="mb-2">
We think we&apos;re pretty great to work with, but don&apos;t take
our word for it. Here&apos;s what our clients have to say.
</p>
<Rule />
<ol className="relative border-s border-[--primary] w-4/5 m-auto">
{Testimonials.sort(
(a, b) => b.date.getTime() - a.date.getTime(),
).map((review) => (
<Review
key={review.date.toISOString()}
name={review.name}
date={review.date}
content={review.content}
sourceIcon={review.sourceIcon}
sourceUrl={review.sourceUrl}
sourceName={review.sourceName}
/>
))}
</ol>
</section>
</main>
</>
);
};
export default Reviews;

View File

@ -1,4 +0,0 @@
a {
font-size: 2rem;
margin: auto 10px;
}

View File

@ -1,7 +0,0 @@
<a
href="{{ social.link }}"
target="_blank"
rel="noopener noreferrer"
attr.aria-label="{{ social.label }}"
><fa-icon attr.alt="{{ social.alt }}" [icon]="social.icon"></fa-icon
></a>

View File

@ -1,49 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Socials } from "../config/Socials";
import { SocialComponent } from "./social.component";
describe("SocialComponent", () => {
let component: SocialComponent;
let fixture: ComponentFixture<SocialComponent>;
let compiled: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SocialComponent]
}).compileComponents();
fixture = TestBed.createComponent(SocialComponent);
component = fixture.componentInstance;
});
for (const expected of Socials) {
it(`should parse ${expected.label} properties correctly`, () => {
component.social = expected;
fixture.detectChanges();
expect(component.social).toBe(expected);
expect(component.link).toBe(expected.link);
expect(component.alt).toBe(expected.alt);
expect(component.icon).toEqual(expected.icon);
expect(component.label).toBe(expected.label);
});
it("should render an icon correctly", () => {
component.social = expected;
fixture.detectChanges();
compiled = fixture.nativeElement;
const link = compiled.querySelector("a");
expect(link?.href).toContain(expected.link);
expect(link?.rel).toBe("noopener noreferrer");
expect(link?.target).toBe("_blank");
expect(link?.ariaLabel).toBe(expected.label);
const icon = compiled.querySelector("fa-icon");
const svg = icon?.querySelector("svg");
const path = svg?.querySelector("path");
expect(path?.getAttribute("d")).toBe(expected.icon.icon[4] as string);
});
}
});

View File

@ -1,33 +0,0 @@
import { Component, Input, OnInit } from "@angular/core";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { Socials } from "../config/Socials";
/**
*
*/
@Component({
selector: "app-social",
standalone: true,
imports: [FontAwesomeModule],
templateUrl: "./social.component.html",
styleUrl: "./social.component.css"
})
export class SocialComponent implements OnInit {
@Input() social: (typeof Socials)[number] = {} as never;
public link = "";
public label = "";
public alt = "";
public icon: IconDefinition = {} as never;
/**
*
*/
ngOnInit(): void {
this.link = this.social.link;
this.label = this.social.label;
this.alt = this.social.alt;
this.icon = this.social.icon;
}
}

View File

@ -1,8 +0,0 @@
p {
max-width: 1200px;
margin: auto;
}
.bold {
font-weight: bold;
}

View File

@ -1,5 +0,0 @@
<h2>Profile Stats</h2>
<p *ngFor="let stat of stats">
<span class="bold">{{ stat.title }}</span
>: {{ stat.value }}
</p>

View File

@ -1,22 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { StatsComponent } from "./stats.component";
describe("StatsComponent", () => {
let component: StatsComponent;
let fixture: ComponentFixture<StatsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [StatsComponent]
}).compileComponents();
fixture = TestBed.createComponent(StatsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,18 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Stats } from "../config/Stats";
/**
*
*/
@Component({
selector: "app-stats",
standalone: true,
imports: [CommonModule],
templateUrl: "./stats.component.html",
styleUrl: "./stats.component.css"
})
export class StatsComponent {
public stats = Stats;
}

View File

@ -1,5 +0,0 @@
.desc {
max-width: 1200px;
margin: auto;
font-size: 1.5rem;
}

View File

@ -1,10 +0,0 @@
<h2>Testimonials</h2>
<p class="desc">
Here's what people have to say about our work! If you want to leave your own,
you can do so through LinkedIn, TopMate, or even an issue on this repository!
</p>
<app-review
*ngFor="let review of testimonials"
[review]="review"
[even]="testimonials.indexOf(review) % 2 === 0"
></app-review>

View File

@ -1,38 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Testimonials } from "../config/Testimonials";
import { TestimonialsComponent } from "./testimonials.component";
describe("TestimonialsComponent", () => {
let component: TestimonialsComponent;
let fixture: ComponentFixture<TestimonialsComponent>;
let compiled: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TestimonialsComponent]
}).compileComponents();
fixture = TestBed.createComponent(TestimonialsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should have expected properties", () => {
expect(component.testimonials).toEqual(
Testimonials.sort((a, b) => b.date.getTime() - a.date.getTime())
);
});
it("should render reviews correctly", () => {
compiled = fixture.nativeElement;
const reviews = compiled.querySelectorAll("app-review");
expect(reviews).toHaveSize(Testimonials.length);
for (let i = 0; i < component.testimonials.length; i++) {
const review = reviews[i];
const content = review.querySelector("article");
expect(content?.textContent).toBe(component.testimonials[i].content);
}
});
});

View File

@ -1,19 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Testimonials } from "../config/Testimonials";
import { ReviewComponent } from "../review/review.component";
/** */
@Component({
selector: "app-testimonials",
standalone: true,
imports: [CommonModule, ReviewComponent],
templateUrl: "./testimonials.component.html",
styleUrl: "./testimonials.component.css"
})
export class TestimonialsComponent {
public testimonials = Testimonials.sort(
(a, b) => b.date.getTime() - a.date.getTime()
);
}

View File

@ -1,28 +0,0 @@
.timeline {
position: relative;
max-width: 1200px;
margin: auto;
}
.desc {
max-width: 1200px;
margin: auto;
font-size: 1.5rem;
}
.timeline::after {
content: "";
position: absolute;
width: 6px;
background-color: var(--text);
top: 0;
bottom: 0;
left: 50%;
margin-left: -3px;
}
@media screen and (max-width: 600px) {
.timeline::after {
left: 31px;
}
}

View File

@ -1,13 +0,0 @@
<h2>Resume</h2>
<p class="desc">
Here you can see a timeline of all of my client work. Items without an end
date (green items) are roles I am still in.
</p>
<div class="timeline">
<div *ngFor="let job of employment">
<app-job
[job]="job"
[align]="employment.indexOf(job) % 2 ? 'right' : 'left'"
/>
</div>
</div>

View File

@ -1,38 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Employment } from "../config/Employment";
import { TimelineComponent } from "./timeline.component";
describe("TimelineComponent", () => {
let component: TimelineComponent;
let fixture: ComponentFixture<TimelineComponent>;
let compiled: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TimelineComponent]
}).compileComponents();
fixture = TestBed.createComponent(TimelineComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should have expected properties", () => {
expect(component.employment).toEqual(
Employment.sort((a, b) => b.start.getTime() - a.start.getTime())
);
});
it("should render timeline correctly", () => {
compiled = fixture.nativeElement;
const jobs = compiled.querySelectorAll("app-job");
expect(jobs).toHaveSize(Employment.length);
for (let i = 0; i < component.employment.length; i++) {
const job = jobs[i];
const title = job.querySelector("h2");
expect(title?.textContent).toBe(component.employment[i].title);
}
});
});

View File

@ -1,21 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Employment } from "../config/Employment";
import { JobComponent } from "../job/job.component";
/**
*
*/
@Component({
selector: "app-timeline",
standalone: true,
imports: [CommonModule, JobComponent],
templateUrl: "./timeline.component.html",
styleUrl: "./timeline.component.css"
})
export class TimelineComponent {
public employment = Employment.sort(
(a, b) => b.start.getTime() - a.start.getTime()
);
}

93
src/app/work/page.tsx Normal file
View File

@ -0,0 +1,93 @@
import { Job } from "@/components/job";
import { Rule } from "@/components/rule";
import { Social } from "@/components/social";
import { Jobs } from "@/config/Jobs";
import { Donate, HireMe, Socials } from "@/config/Socials";
import { Volunteer } from "@/icons/Volunteer";
import { faCalendar, faTasks } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const Work = (): JSX.Element => {
return (
<>
<main className="w-4/5 text-center max-w-4xl m-auto mt-16 mb-16 rounded-lg">
<h1 className="text-5xl">Our Work</h1>
<p>
We run a software engineering and community management firm known as
nhcarrigan.
</p>
<Rule />
<section>
<h2 className="text-3xl">Legend</h2>
<p>
Our work is listed here in reverse chronological order. The symbols
and colours have a specific meaning:
</p>
<table className="m-auto w-1/2">
<thead>
<tr>
<th>Symbol</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<FontAwesomeIcon className="h-14" icon={faCalendar} />
</td>
<td>Fixed-Rate Contract (hourly/salary)</td>
</tr>
<tr>
<td>
<FontAwesomeIcon className="h-14" icon={faTasks} />
</td>
<td>Project-based Contract</td>
</tr>
<tr>
<td>
<FontAwesomeIcon className="h-14" icon={Volunteer} />
</td>
<td>Planned</td>
</tr>
<tr>
<td>
<p className="h-14 w-14 border-[--current] border-solid border-2"></p>
</td>
<td className="text-[--current]">Current Contract</td>
</tr>
<tr>
<td>
<p className="h-14 w-14 border-[--former] border-dashed border-2"></p>
</td>
<td className="text-[--former]">Former Contract</td>
</tr>
</tbody>
</table>
<Rule />
</section>
<section>
<h2 className="text-3xl">Timeline</h2>
<ol className="relative border-s border-[--primary] w-4/5 m-auto">
{Jobs.sort((a, b) => b.start.getTime() - a.start.getTime()).map(
(job) => (
<Job
key={job.title + " - " + job.company}
title={job.title}
company={job.company}
start={job.start}
end={job.end}
link={job.link}
type={job.type}
description={job.description}
logo={job.logo}
/>
),
)}
</ol>
</section>
</main>
</>
);
};
export default Work;

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

View File

@ -1,94 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" width="45.6871mm" height="8.8199mm" viewBox="0 0 259 50">
<path id="Selection" fill="#abfcec" stroke="black" stroke-width="1" stroke-opacity="0" d="M 0.00,0.00
C 6.77,0.00 19.27,-1.03 24.99,2.05
36.43,8.22 39.43,26.63 26.99,35.15
19.43,40.32 8.80,39.00 0.00,39.00
2.13,36.04 4.45,32.92 8.00,31.60
11.21,30.40 17.85,31.66 22.78,26.78
28.64,20.96 26.82,14.03 20.00,10.09
16.14,7.86 12.27,8.01 8.00,8.00
8.00,8.00 8.00,21.00 8.00,21.00
8.00,21.00 0.00,21.00 0.00,21.00
0.00,21.00 0.00,0.00 0.00,0.00 Z
M 65.00,29.00
C 62.63,40.09 49.59,41.43 43.19,36.35
38.88,32.94 37.57,27.25 38.11,22.00
39.26,10.60 52.90,7.73 59.90,12.72
65.00,16.37 64.98,21.39 65.00,27.00
65.00,27.00 46.00,27.00 46.00,27.00
50.50,35.50 54.58,30.75 58.28,29.51
60.20,28.88 62.96,29.01 65.00,29.00 Z
M 96.00,29.00
C 92.82,43.92 69.87,41.91 69.04,26.00
68.13,8.49 85.75,8.02 91.66,13.43
95.74,17.16 95.95,21.90 96.00,27.00
96.00,27.00 76.00,27.00 76.00,27.00
82.93,35.49 84.45,31.13 89.28,29.51
91.20,28.88 93.96,29.01 96.00,29.00 Z
M 109.00,13.00
C 126.25,0.73 137.33,31.29 121.00,37.99
118.90,38.85 116.25,39.10 114.00,38.89
111.35,38.65 110.33,38.08 108.00,37.00
108.00,37.00 108.00,50.00 108.00,50.00
108.00,50.00 101.02,48.40 101.02,48.40
101.02,48.40 100.00,41.00 100.00,41.00
100.00,41.00 100.00,11.00 100.00,11.00
104.11,11.01 105.54,10.53 109.00,13.00 Z
M 152.00,37.00
C 148.66,38.55 146.77,39.22 143.00,38.89
129.80,37.76 131.09,21.05 133.17,17.02
136.52,10.52 145.36,8.21 151.00,13.00
154.46,10.53 155.89,11.01 160.00,11.00
160.00,11.00 160.00,38.00 160.00,38.00
159.94,41.76 159.91,44.72 156.77,47.35
152.24,51.15 140.90,50.00 135.00,50.00
140.86,38.04 149.88,49.96 152.00,37.00 Z
M 186.00,21.00
C 188.17,8.02 203.28,8.27 208.30,14.34
212.19,19.05 211.00,32.75 211.00,39.00
207.15,38.99 204.61,39.88 203.00,36.00
193.53,42.24 184.84,37.41 186.29,29.02
188.09,18.66 198.04,25.75 203.00,18.00
192.55,14.35 197.53,20.60 186.00,21.00 Z
M 223.00,14.00
C 230.14,8.80 233.45,9.36 240.00,15.00
245.00,9.48 253.73,8.31 257.35,16.02
259.69,21.01 259.00,33.09 259.00,39.00
259.00,39.00 251.00,39.00 251.00,39.00
251.00,34.48 252.85,18.20 246.00,18.20
239.15,18.20 241.00,34.48 241.00,39.00
241.00,39.00 233.00,39.00 233.00,39.00
233.00,34.50 234.84,18.20 228.02,18.20
225.53,18.20 224.01,20.92 223.21,23.00
222.76,25.42 223.00,35.81 223.21,39.00
223.21,39.00 215.00,39.00 215.00,39.00
215.00,39.00 215.00,11.00 215.00,11.00
218.85,11.01 221.39,10.12 223.00,14.00 Z
M 166.00,11.00
C 170.11,11.01 171.54,10.53 175.00,13.00
175.00,13.00 184.00,11.00 184.00,11.00
184.00,11.00 184.00,17.00 184.00,17.00
170.90,17.77 174.00,29.35 174.00,39.00
174.00,39.00 166.00,39.00 166.00,39.00
166.00,39.00 166.00,11.00 166.00,11.00 Z
M 57.00,21.00
C 53.77,14.97 49.18,14.83 46.00,21.00
46.00,21.00 57.00,21.00 57.00,21.00 Z
M 88.00,21.00
C 84.91,15.23 79.58,14.32 77.00,21.00
77.00,21.00 88.00,21.00 88.00,21.00 Z
M 113.04,17.70
C 105.69,21.66 107.40,30.41 113.04,31.67
122.24,33.71 124.93,17.02 113.04,17.70 Z
M 145.04,17.54
C 135.47,21.27 139.98,33.59 147.89,31.55
153.87,30.01 155.20,17.81 145.04,17.54 Z
M 203.00,27.03
C 203.00,27.03 199.02,27.03 199.02,27.03
190.18,27.85 196.44,36.30 201.28,30.69
202.34,29.46 202.50,28.42 203.00,27.03 Z" />
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Some files were not shown because too many files have changed in this diff Show More