commit 3ec7d9a72237a1d9fad4d9822ac46e430de11196 Author: Eugene Fox Date: Mon Aug 19 23:08:50 2024 +0000 init: First version diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..3c9fefb --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,31 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "my-website", + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye", + + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "version": "latest", + "dockerDashComposeVersion": "none" + } + }, + + "postCreateCommand": "yarn install", + + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "bierner.github-markdown-preview", + "dbaeumer.vscode-eslint", + "github.vscode-github-actions", + "Gruntfuggly.todo-tree", + "jock.svg", + "mrmlnc.vscode-scss", + "ms-azuretools.vscode-docker", + "saeris.markdown-github-alerts" + ] + } + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..234e453 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.devcontainer/ +.github/ +.next/ +.vscode/ +node_modules/ + +.env* +next-env.d.ts + +*.md +LICENSE +COPYING + +.git +.*ignore +Dockerfile* +*.log diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..71f6fd8 --- /dev/null +++ b/.env.development @@ -0,0 +1,13 @@ +# .env file template (do not modify) +# Copy this file to .env, .env.local, or .env.production and fill in the values + +# Mail credentials for redirecting form inquiries (see app/_utils/sendInquiry.ts) +SMTP_HOST=mailserver # Address of your SMTP server +SMTP_PORT=port # Port of your SMTP server +SMTP_USER=username # Username of your email bot account (usually same, as email address) +SMTP_PASSWORD=password # Password of your email bot account +SMTP_FROM_EMAIL=email # Email address which will be displayed in "From" field +SMTP_TO_EMAIL=email # Email to which emails will be sent + +RESUME_URL=URL # Location of the resume PDF +CLARITY_ID=string # Clarity Analytics ID (optional, remove to disable) diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..3ba6997 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,35 @@ +{ + "extends": [ + "next/core-web-vitals", + "eslint:recommended" + ], + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "rules": { + "react/no-unescaped-entities": "off", + "indent": [ + "warn", + "tab", + { + "SwitchCase": 1 + } + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ], + "no-unused-vars": "warn", + "react/react-in-jsx-scope": "off", + "react/jsx-uses-react": "off", + "no-unreachable": "warn" + } +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..a009116 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,99 @@ +name: "🐞 Bug Report" +description: Create a report to help us improve the website +title: "[Bug]: " +labels: ["bug", "needs-triage"] +assignees: + - xfox111 +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + attributes: + label: Reproduction steps + description: Precisely describe minimal number of steps that make the bug to appear + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See '...' + validations: + required: true + + - type: textarea + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + + - type: textarea + attributes: + label: Screenshot + description: If applicable, add screenshots to help explain your problem. + validations: + required: false + + - type: dropdown + id: os + attributes: + label: Operating system + options: + - "Windows 10 and newer" + - "Windows 8/8.1" + - "Windows 7 and older" + - "Android" + - "MacOS" + - "iOS/iPadOS" + - "Debian or Debian-based" + - "Other" + validations: + required: true + + - type: input + id: browser + attributes: + label: Browser name and version + placeholder: e.g. Microsoft Edge 119.0.2151.58 + description: Put here your browser's name and version + validations: + required: true + + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context about the problem here. + validations: + required: false + + - type: dropdown + id: pr + attributes: + label: Are you willing to submit a PR for this issue? + options: + - "yes" + - "no" + validations: + required: true + + - type: checkboxes + id: checkboxes + attributes: + label: Validations + description: Before submitting the issue, please make sure you do the following + options: + - label: Check that there isn't already an issue that reports the same bug to avoid creating a duplicate. + required: true + - label: The provided reproduction is a minimal reproducible example of the bug. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..9e6deba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,3 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json + +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..6c39fd1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,62 @@ +name: "🚀 New feature proposal" +description: Suggest a feature idea for the website +title: "[Feature]: " +labels: ["feature", "needs-triage"] +assignees: + - xfox111 +body: + - type: markdown + attributes: + value: | + Thanks for your interest and taking the time to fill out this form! + + - type: textarea + id: proposition + attributes: + label: Proposed solution + description: Describe the solution you'd like + validations: + required: true + + - type: textarea + id: justification + attributes: + label: Justification + description: Is your feature request related to a problem? Please describe. + validations: + required: true + + - type: textarea + id: alts + attributes: + label: Alternatives + description: Describe alternatives you've considered. + validations: + required: true + + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + validations: + required: false + + - type: dropdown + id: requested-help + attributes: + label: Are you willing to submit a PR for this issue? + options: + - "yes" + - "no" + validations: + required: true + + - type: checkboxes + id: checkboxes + attributes: + label: Validations + description: Before submitting the issue, please make sure you do the following + options: + - label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate. + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..407b669 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,55 @@ +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +# yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json + +version: 2 +updates: + + - package-ecosystem: "npm" + directory: "/" + target-branch: "main" + assignees: + - "xfox111" + reviewers: + - "xfox111" + schedule: + interval: monthly + rebase-strategy: disabled + open-pull-requests-limit: 20 + + - package-ecosystem: "github-actions" + directory: "/" + target-branch: "main" + assignees: + - "xfox111" + reviewers: + - "xfox111" + schedule: + interval: monthly + rebase-strategy: disabled + open-pull-requests-limit: 20 + + - package-ecosystem: "devcontainers" + directory: "/" + target-branch: "main" + assignees: + - "xfox111" + reviewers: + - "xfox111" + schedule: + interval: monthly + rebase-strategy: disabled + open-pull-requests-limit: 20 + + - package-ecosystem: "docker" + directory: "/" + target-branch: "main" + assignees: + - "xfox111" + reviewers: + - "xfox111" + schedule: + interval: monthly + rebase-strategy: disabled + open-pull-requests-limit: 20 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..fa8e90d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ +Resolves: #issue_number + +# Description + + + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1facdb9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: "CI pipeline" + +on: + push: + branches: [ "main" ] + paths-ignore: + - '.devcontainer/*' + - '.github/*' + - '!.github/workdlows/ci.yml' + - '.vscode/*' + - '**.md' + - 'LICENSE' + - 'COPYING' + pull_request: + branches: [ "main" ] + paths-ignore: + - '.devcontainer/*' + - '.github/*' + - '!.github/workdlows/ci.yml' + - '.vscode/*' + - '**.md' + - 'LICENSE' + - 'COPYING' + workflow_dispatch: + inputs: + push: + type: boolean + required: false + default: false + description: "Push to Docker Hub" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: "Login to Docker Hub" + if: github.event_name != 'pull_request' || github.event.inputs.push == 'true' + uses: docker/login-action@v3 + with: + username: ${{ github.repository_owner }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: "Login to GitHub Container Registry" + if: github.event_name != 'pull_request' || github.event.inputs.push == 'true' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.event_name != 'pull_request' || github.event.inputs.push == 'true' }} + tags: | + ${{ github.repository }} + ghcr.io/${{ github.repository }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9a22e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env* +!.env.development + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c22c281 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,12 @@ +{ + "recommendations": [ + "bierner.github-markdown-preview", + "dbaeumer.vscode-eslint", + "github.vscode-github-actions", + "Gruntfuggly.todo-tree", + "jock.svg", + "mrmlnc.vscode-scss", + "ms-azuretools.vscode-docker", + "saeris.markdown-github-alerts" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..212a1c3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "editor.rulers": [ + { + "column": 120 + } + ], + "editor.insertSpaces": false, + "files.insertFinalNewline": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + }, + "files.eol": "\n", + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "todo-tree.filtering.excludeGlobs": [ + "**/node_modules/*/**", + "README.md" + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..f2bb98e --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,60 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "build", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [], + "label": "yarn: build", + "detail": "Build project" + }, + { + "type": "npm", + "script": "install", + "group": "build", + "problemMatcher": [], + "label": "yarn: install", + "detail": "Restore dependencies" + }, + { + "type": "npm", + "script": "dev", + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": [], + "label": "yarn: dev", + "detail": "Start development server" + }, + { + "type": "npm", + "script": "lint", + "group": "test", + "problemMatcher": [], + "label": "yarn: lint", + "detail": "Run ESLint" + }, + { + "type": "shell", + "command": "docker", + "args": [ + "build", + "-t", + "my-website", + "./" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "group": "build", + "problemMatcher": [], + "label": "docker: build", + "detail": "Build a Docker image" + }, + ] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..18231ab --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,135 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[opensource@xfox111.net](mailto:opensource@xfox111.net). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +> Contributor Covenant is released under the [Creative Commons Attribution 4.0 International Public License](https://github.com/EthicalSource/contributor_covenant/blob/release/LICENSE.md). + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..eb22ec2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contribution Guidelines + +Thank you for considering contributing to the project! We welcome your help and appreciate your support. + +## General guidelines + +- **Follow templates:** Use the provided issue and pull request templates to ensure consistency and completeness. Be concise. +- **Follow Single responisibility principle:** Each contribution must correspond to a single area of work. Don't mix them up (e.g. fixing a bug and adding a new feature in the same PR). +- **Code of Conduct:** Be sure to follow our [Code of Conduct](CODE_OF_CONDUCT.md). + +## Contributing Process + +1. **Create an Issue:** Before starting work, check the issue tracker to see if a relevant issue already exists. If not, create a new issue to describe the bug or feature. + +2. **Get Assigned:** Make sure you are assigned to an issue before beginning any work. This helps prevent duplicate efforts. + +3. **Fork the Repository:** Fork the repository and clone it to your local machine. + +4. **Work on Your Changes:** Create a new branch for your work and make your changes. + +5. **Commit Your Changes:** Write clear, concise commit messages. + +6. **Open a Pull Request (PR):** Once your changes are ready, open a PR. Ensure you link the PR to the corresponding issue. + +7. **Review Process:** Be responsive to feedback and make any requested changes. + +## Additional Notes + +Feel free to suggest improvements or ask questions. You can send your feedback to [opensource@xfox111.net](mailto:opensource@xfox111.net) + +Thank you! diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..c247d4b --- /dev/null +++ b/COPYING @@ -0,0 +1,24 @@ +©2024 Eugene Fox. Some rights reserved. + +Following files and directories are excempt from MIT license coverage +and are subjects to general copyright law: + + - /app/_assets + - /app/_data + - /app/apple-icon.png + - /app/favicon.ico + - /app/icon.svg + - /app/opengraph-image.alt.txt + - /app/opengraph-image.png + +You must obtain a written permission from the author in order to use +any copyrighted material. + +You may use copyrighted material without excplicit permission +in following cases: + + - Educational purposes. + - Any other cases which may be deemed as a fair use. + +When shared or modified, each copyrighted material must have a proper +attribution. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eec5055 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies +COPY package.json yarn.lock ./ +RUN yarn --frozen-lockfile + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +# ENV NEXT_TELEMETRY_DISABLED 1 + +RUN yarn lint +RUN yarn build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +# Uncomment the following line in case you want to disable telemetry during runtime. +# ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +# server.js is created by next build from the standalone output +# https://nextjs.org/docs/pages/api-reference/next-config-js/output +CMD [ "node", "server.js" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7452667 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Eugene Fox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8513286 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# xfox111.net website + +[![Website status](https://img.shields.io/website?url=http%3A//xfox111.net/)](https://xfox111.net) +[![GitHub last commit](https://img.shields.io/github/last-commit/xfox111/my-website?label=Last+update)](https://github.com/XFox111/my-website/commits/main) +[![Docker Image Size](https://img.shields.io/docker/image-size/xfox111/my-website?logo=docker&logoColor=white)](https://hub.docker.com/r/xfox111/my-website/) + + + + + xfox111.net website + + +This repository contains the source code for my personal website, built using Next.js. The website serves as a portfolio, showcasing my projects skills. Feel free to use this code as a base or a template for your own personal website. + +## Features + +- **Responsive Design:** Optimized for desktop and mobile devices. +- **Dark Mode:** Automatic light and dark themes based on your browser preferences. +- **Accessibility:** Full keyboard navigation and screen reader support. +- **Customizable:** You can use this website as a template for your own personal website. +- **Docker and Dev Containers:** Containerized development and deployment. + +## Technologies + +- [Next.js](https://nextjs.org/) framework for server-side rendering and static site generation. +- [React](https://reactjs.org/) library for building user interfaces. +- [SASS](https://sass-lang.com/) preprocessor. +- [TypeScript](https://www.typescriptlang.org/). + +## Development + +### Prerequisites + +For development you can use [Dev Containers](https://devcontainers.io/) or [GitHub Codespaces](https://github.com/features/codespaces). Otherwise you will need to install following tools: +- [Node.js](https://nodejs.org/en/) +- [Yarn](https://yarnpkg.com/) +- [Docker](https://www.docker.com/) + + +### Building and debugging + +Here're some commonly used commands: +```bash +yarn install # Install dependencies +yarn dev # Start the development server at http://localhost:3000 +yarn lint # Lint the project with ESLint +yarn build # Build the project for production +``` + +To build a Docker image, run: + +```bash +docker build -t . +``` + +> [!TIP] +> If you use VS Code, you can also use pre-defined tasks for building and debugging. + +## Customization +You can customize the website by modifying its components and styles. + +Here's a general checklist of things you need to change: + +### Environment +- [ ] `package.json` (URLs and author information) +- [ ] `.env.*` (required for website to function) + +### Assets +- [ ] `/app/favicon.ico` +- [ ] `/app/icon.svg` +- [ ] `/app/apple-icon.png` +- [ ] `/app/opengraph-image.png` +- [ ] `/app/opengraph-image.alt.txt` +- [ ] `/app/_assets` + +### Content +- [ ] `app/_data` (text information and some page elements) +- [ ] (optional) `app/attribution/page.tsx` (license and attribution information) +- [ ] (optional) `app/theme.[light|dark].scss` (color schemes) +- [ ] (optional) Files marked with `[SPECIAL]` tag (these contain custom elements, which may be not suitable for your needs) + +> [!IMPORTANT] +> Some of the files are copyrighted and should not be used without permission. See [COPYING](/COPYING). + +## Contributing +Contributions are welcome! If you have suggestions or improvements, feel free to open an issue or submit a pull request. + +> [!NOTE] +> Please make sure to follow the [contributing guidelines](/CONTRIBUTING.md) + +## License + +This repository is partially licensed under [MIT license](/LICENSE). + +> [!IMPORTANT] +> Some content of this repository is exempt from MIT license coverage and is subject to general copyright law. See [COPYING](/COPYING) for more information. + +--- + +[![Twitter Follow](https://img.shields.io/twitter/follow/xfox111?style=social)](https://twitter.com/xfox111) +[![GitHub followers](https://img.shields.io/github/followers/xfox111?label=Follow%20@xfox111&style=social)](https://github.com/xfox111) +[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-%40xfox111-orange)](https://buymeacoffee.com/xfox111) + +> ©2024 Eugene Fox. Some rights reserved diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ed1120a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +This is a personal website project, so it doesn't really require policies on reporting vulnerabilities. +But in case you have found one, I will be glad if you report it :) + +## Reporting vulnerabilities + +Here's a basic guideline on how to do this: + +1. Send an email to opensource@xfox111.net +1. Give a clear description of the issue, including how to reproduce it and what impact it might have. +1. If you can, include a proof-of-concept or screenshot showing the problem. + +I'll do my best to address any issues and give you an update. + +Thank you <3 diff --git a/app/_assets/assets.ts b/app/_assets/assets.ts new file mode 100644 index 0000000..aa8c095 --- /dev/null +++ b/app/_assets/assets.ts @@ -0,0 +1,7 @@ +import { StaticImageData } from "next/image"; + +export type ImageExport = + { + src: string | StaticImageData; + alt: string; + }; diff --git a/app/_assets/decorations.ts b/app/_assets/decorations.ts new file mode 100644 index 0000000..b353016 --- /dev/null +++ b/app/_assets/decorations.ts @@ -0,0 +1,31 @@ +import { ImageExport } from "./assets"; +import pageNotFox from "./decorations/page-not-fox.svg"; +import pawprintTrailVertical from "./decorations/pawprint-trail-vertical.svg"; +import pawprintTrail from "./decorations/pawprint-trail.svg"; + +export const textCorrection: ImageExport = +{ + src: pageNotFox, + alt: "Page, not fox" +}; + +export const experienceBgVertical: ImageExport = +{ + src: pawprintTrailVertical, + alt: "" +}; + +export const experienceBgHorizontal: ImageExport = +{ + src: pawprintTrail, + alt: "" +}; + +const decorations = +{ + textCorrection, + experienceBgVertical, + experienceBgHorizontal +}; + +export default decorations; diff --git a/app/_assets/decorations/page-not-fox.svg b/app/_assets/decorations/page-not-fox.svg new file mode 100644 index 0000000..c182c18 --- /dev/null +++ b/app/_assets/decorations/page-not-fox.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/app/_assets/decorations/pawprint-trail-vertical.svg b/app/_assets/decorations/pawprint-trail-vertical.svg new file mode 100644 index 0000000..77d5393 --- /dev/null +++ b/app/_assets/decorations/pawprint-trail-vertical.svg @@ -0,0 +1,445 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/decorations/pawprint-trail.svg b/app/_assets/decorations/pawprint-trail.svg new file mode 100644 index 0000000..27efe37 --- /dev/null +++ b/app/_assets/decorations/pawprint-trail.svg @@ -0,0 +1,446 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations.ts b/app/_assets/illustrations.ts new file mode 100644 index 0000000..8a97763 --- /dev/null +++ b/app/_assets/illustrations.ts @@ -0,0 +1,95 @@ +import { ImageExport } from "./assets"; +import netResume from "./illustrations/dotnet-resume.svg"; +import footer from "./illustrations/footer.svg"; +import foxRuns from "./illustrations/fox_runs.gif"; +import fullstackResumeImg from "./illustrations/fullstack-resume.svg"; +import fox from "./illustrations/home-decor.svg"; +import itsMe from "./illustrations/its_me.webp"; +import meKvant from "./illustrations/me_kvant.webp"; +import nextLogo from "./illustrations/next-logo.svg"; +import notFound from "./illustrations/not-found.svg"; +import petProjects from "./illustrations/pet-projects.svg"; +import reactResumeImg from "./illustrations/react-resume.svg"; + +export const dotnetResume: ImageExport = +{ + src: netResume, + alt: "Fox riding on a back of angry .NET bot" +}; + +export const fullstackResume: ImageExport = +{ + src: fullstackResumeImg, + alt: "Fox balancing on a React logo, while happy .NET bot is sitting on his back" +}; + +export const reactResume: ImageExport = +{ + src: reactResumeImg, + alt: "Fox laying on his back and playing with a React logo" +}; + +export const footerImage: ImageExport = +{ + src: footer, + alt: "A cartoon fox looking at a laptop" +}; + +export const spinner: ImageExport = +{ + src: foxRuns, + alt: "Cute cartoon fox runs to the left" +}; + +export const homeDecor: ImageExport = +{ + src: fox, + alt: "A cartoon fox working at a laptop" +}; + +export const profilePicture: ImageExport = +{ + src: itsMe, + alt: "Photo of a young person with glasses, red hair, red shirt and computers on the background" +}; + +export const aboutPicture: ImageExport = +{ + src: meKvant, + alt: "Photo of a young person sitting on a pong table with headphones and a badge. Behind them is a cartoon 2D fox looking at them." +}; + +export const nextjsLogo: ImageExport = +{ + src: nextLogo, + alt: "Next.js" +}; + +export const notFoundImage: ImageExport = +{ + src: notFound, + alt: "Cardboard box put upside down with a fox tail seen from beneath" +}; + +export const projectsImg: ImageExport = +{ + src: petProjects, + alt: "Cartoon fox looking curiously at the incubator with chickens" +}; + +const illustrations = +{ + footerImage, + spinner, + homeDecor, + profilePicture, + aboutPicture, + nextjsLogo, + notFoundImage, + projectsImg, + dotnetResume, + fullstackResume, + reactResume +}; + +export default illustrations; diff --git a/app/_assets/illustrations/dotnet-resume.svg b/app/_assets/illustrations/dotnet-resume.svg new file mode 100644 index 0000000..37392c2 --- /dev/null +++ b/app/_assets/illustrations/dotnet-resume.svg @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/footer.svg b/app/_assets/illustrations/footer.svg new file mode 100644 index 0000000..2a05139 --- /dev/null +++ b/app/_assets/illustrations/footer.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/fox_runs.gif b/app/_assets/illustrations/fox_runs.gif new file mode 100644 index 0000000..2aeb12a Binary files /dev/null and b/app/_assets/illustrations/fox_runs.gif differ diff --git a/app/_assets/illustrations/fullstack-resume.svg b/app/_assets/illustrations/fullstack-resume.svg new file mode 100644 index 0000000..724a317 --- /dev/null +++ b/app/_assets/illustrations/fullstack-resume.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/home-decor.svg b/app/_assets/illustrations/home-decor.svg new file mode 100644 index 0000000..1b2fe14 --- /dev/null +++ b/app/_assets/illustrations/home-decor.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/its_me.webp b/app/_assets/illustrations/its_me.webp new file mode 100644 index 0000000..37f8050 Binary files /dev/null and b/app/_assets/illustrations/its_me.webp differ diff --git a/app/_assets/illustrations/me_kvant.webp b/app/_assets/illustrations/me_kvant.webp new file mode 100644 index 0000000..f0bca50 Binary files /dev/null and b/app/_assets/illustrations/me_kvant.webp differ diff --git a/app/_assets/illustrations/next-logo.svg b/app/_assets/illustrations/next-logo.svg new file mode 100644 index 0000000..e1b6e2b --- /dev/null +++ b/app/_assets/illustrations/next-logo.svg @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/_assets/illustrations/not-found.svg b/app/_assets/illustrations/not-found.svg new file mode 100644 index 0000000..12995df --- /dev/null +++ b/app/_assets/illustrations/not-found.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/pet-projects.svg b/app/_assets/illustrations/pet-projects.svg new file mode 100644 index 0000000..5b57f2c --- /dev/null +++ b/app/_assets/illustrations/pet-projects.svg @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/projects/EasyLogon.svg b/app/_assets/illustrations/projects/EasyLogon.svg new file mode 100644 index 0000000..30c960e --- /dev/null +++ b/app/_assets/illustrations/projects/EasyLogon.svg @@ -0,0 +1,1843 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/projects/FoxTube/FoxTube-dark.webp b/app/_assets/illustrations/projects/FoxTube/FoxTube-dark.webp new file mode 100644 index 0000000..fafa709 Binary files /dev/null and b/app/_assets/illustrations/projects/FoxTube/FoxTube-dark.webp differ diff --git a/app/_assets/illustrations/projects/FoxTube/FoxTube-light.webp b/app/_assets/illustrations/projects/FoxTube/FoxTube-light.webp new file mode 100644 index 0000000..161a73f Binary files /dev/null and b/app/_assets/illustrations/projects/FoxTube/FoxTube-light.webp differ diff --git a/app/_assets/illustrations/projects/FoxTube/dark.webp b/app/_assets/illustrations/projects/FoxTube/dark.webp new file mode 100644 index 0000000..9b6c5ec Binary files /dev/null and b/app/_assets/illustrations/projects/FoxTube/dark.webp differ diff --git a/app/_assets/illustrations/projects/FoxTube/light.webp b/app/_assets/illustrations/projects/FoxTube/light.webp new file mode 100644 index 0000000..769628f Binary files /dev/null and b/app/_assets/illustrations/projects/FoxTube/light.webp differ diff --git a/app/_assets/illustrations/projects/GUTSchedule.svg b/app/_assets/illustrations/projects/GUTSchedule.svg new file mode 100644 index 0000000..90cd106 --- /dev/null +++ b/app/_assets/illustrations/projects/GUTSchedule.svg @@ -0,0 +1,1005 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/projects/MotionDecoder/MotionDecoder-dark.webp b/app/_assets/illustrations/projects/MotionDecoder/MotionDecoder-dark.webp new file mode 100644 index 0000000..65f0044 Binary files /dev/null and b/app/_assets/illustrations/projects/MotionDecoder/MotionDecoder-dark.webp differ diff --git a/app/_assets/illustrations/projects/MotionDecoder/MotionDecoder-light.webp b/app/_assets/illustrations/projects/MotionDecoder/MotionDecoder-light.webp new file mode 100644 index 0000000..be63634 Binary files /dev/null and b/app/_assets/illustrations/projects/MotionDecoder/MotionDecoder-light.webp differ diff --git a/app/_assets/illustrations/projects/MotionDecoder/dark.webp b/app/_assets/illustrations/projects/MotionDecoder/dark.webp new file mode 100644 index 0000000..6bbd78e Binary files /dev/null and b/app/_assets/illustrations/projects/MotionDecoder/dark.webp differ diff --git a/app/_assets/illustrations/projects/MotionDecoder/light.webp b/app/_assets/illustrations/projects/MotionDecoder/light.webp new file mode 100644 index 0000000..5cfbe44 Binary files /dev/null and b/app/_assets/illustrations/projects/MotionDecoder/light.webp differ diff --git a/app/_assets/illustrations/projects/PasswordGenerator/PasswordGeneratorExtension-dark.webp b/app/_assets/illustrations/projects/PasswordGenerator/PasswordGeneratorExtension-dark.webp new file mode 100644 index 0000000..654f11b Binary files /dev/null and b/app/_assets/illustrations/projects/PasswordGenerator/PasswordGeneratorExtension-dark.webp differ diff --git a/app/_assets/illustrations/projects/PasswordGenerator/PasswordGeneratorExtension-light.webp b/app/_assets/illustrations/projects/PasswordGenerator/PasswordGeneratorExtension-light.webp new file mode 100644 index 0000000..c57ee9e Binary files /dev/null and b/app/_assets/illustrations/projects/PasswordGenerator/PasswordGeneratorExtension-light.webp differ diff --git a/app/_assets/illustrations/projects/PasswordGenerator/dark.webp b/app/_assets/illustrations/projects/PasswordGenerator/dark.webp new file mode 100644 index 0000000..0234552 Binary files /dev/null and b/app/_assets/illustrations/projects/PasswordGenerator/dark.webp differ diff --git a/app/_assets/illustrations/projects/PasswordGenerator/light.webp b/app/_assets/illustrations/projects/PasswordGenerator/light.webp new file mode 100644 index 0000000..a250903 Binary files /dev/null and b/app/_assets/illustrations/projects/PasswordGenerator/light.webp differ diff --git a/app/_assets/illustrations/projects/SimpleOTP-old.svg b/app/_assets/illustrations/projects/SimpleOTP-old.svg new file mode 100644 index 0000000..f5be917 --- /dev/null +++ b/app/_assets/illustrations/projects/SimpleOTP-old.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/projects/SimpleOTP.svg b/app/_assets/illustrations/projects/SimpleOTP.svg new file mode 100644 index 0000000..efd3a84 --- /dev/null +++ b/app/_assets/illustrations/projects/SimpleOTP.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/projects/TabsAside/dark.webp b/app/_assets/illustrations/projects/TabsAside/dark.webp new file mode 100644 index 0000000..0f1284e Binary files /dev/null and b/app/_assets/illustrations/projects/TabsAside/dark.webp differ diff --git a/app/_assets/illustrations/projects/TabsAside/light.webp b/app/_assets/illustrations/projects/TabsAside/light.webp new file mode 100644 index 0000000..864679f Binary files /dev/null and b/app/_assets/illustrations/projects/TabsAside/light.webp differ diff --git a/app/_assets/illustrations/react-resume.svg b/app/_assets/illustrations/react-resume.svg new file mode 100644 index 0000000..7e8c232 --- /dev/null +++ b/app/_assets/illustrations/react-resume.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/skills.ts b/app/_assets/illustrations/skills.ts new file mode 100644 index 0000000..7150f3a --- /dev/null +++ b/app/_assets/illustrations/skills.ts @@ -0,0 +1,63 @@ +import { ImageExport } from "../assets"; +import adminSkills from "./skills/admin-skills.svg"; +import architectureSkills from "./skills/architecture-skills.svg"; +import databasesSkills from "./skills/databases-skills.svg"; +import designSkills from "./skills/design-skills.svg"; +import devopsSkills from "./skills/devops-skills.svg"; +import dotnetSkills from "./skills/dotnet-skills.svg"; +import nodejsSkills from "./skills/nodejs-skills.svg"; + +export const admin: ImageExport = +{ + src: adminSkills, + alt: "Illustration of a server rack with various servers and panels, some displaying graphs and lights, and a large orange fox wrapped around one of the servers, that displays a message saying \"kernel panic\" and \"rebooting\". Below is a laptop with system logs that read: \"rf -rf / in progress\"." +}; + +export const architecture: ImageExport = +{ + src: architectureSkills, + alt: "Cartoon fox draws in a blueprint. Behind him on the left is a chalkboard with a drawing that reads: \"coffee + imaination = magic\". On the right is a drafting table, and a laptop with stickers. The stickers say: \"This is a server, do not turn off\"" +}; + +export const databases: ImageExport = +{ + src: databasesSkills, + alt: "Cartoon fox balancing on a warehouse shelf with different boxes, that have database logos on them (Redis, SQL Server, Mongo, Postgres and MySQL). The shelf is about to crash onto .NET bot, who is relaxed in a chair." +}; + +export const design: ImageExport = +{ + src: designSkills, + alt: "Illustration of a fox wearing a blue beret, painting on a canvas that reads has a green \"Click me\" button, surrounded by colorful paw prints and two paint cans, one labeled \"Ps\", and the other having Figma logo on it." +}; + +export const devops: ImageExport = +{ + src: devopsSkills, + alt: "An orange fox wearing a construction helmet, using a laptop. Behind him is a production line that features Git, unit tests and Docker. There's a wrench falling from one of the containers onto fox's head." +}; + +export const dotnet: ImageExport = +{ + src: dotnetSkills, + alt: "Cartoon fox looking cautiously at .NET bot" +}; + +export const nodejs: ImageExport = +{ + src: nodejsSkills, + alt: "Cartoon fox lifted by balloons representing TypeScript, JavaScript, and React, with a laptop below." +}; + +const skills = +{ + nodejs, + dotnet, + architecture, + databases, + design, + devops, + admin, +}; + +export default skills; diff --git a/app/_assets/illustrations/skills/admin-skills.svg b/app/_assets/illustrations/skills/admin-skills.svg new file mode 100644 index 0000000..1f0f385 --- /dev/null +++ b/app/_assets/illustrations/skills/admin-skills.svg @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + kernel panic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + kernel panic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + srv3:~$ reboot!!! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OwOS v4.11.0 + $ ansible exec + srv1: rm -rf / ... OK + srv2: rm -rf / ... OK + srv3: rm -rf / ... + + + diff --git a/app/_assets/illustrations/skills/architecture-skills.svg b/app/_assets/illustrations/skills/architecture-skills.svg new file mode 100644 index 0000000..fbdad04 --- /dev/null +++ b/app/_assets/illustrations/skills/architecture-skills.svg @@ -0,0 +1,475 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Magic + + + Poof + + + + + + + + + + + + + + + + + + + + + + + + + + + + This is a + server! + + + + DO NOT + TURN + OFF! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/skills/databases-skills.svg b/app/_assets/illustrations/skills/databases-skills.svg new file mode 100644 index 0000000..e641c15 --- /dev/null +++ b/app/_assets/illustrations/skills/databases-skills.svg @@ -0,0 +1,746 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/skills/design-skills.svg b/app/_assets/illustrations/skills/design-skills.svg new file mode 100644 index 0000000..43f9b9d --- /dev/null +++ b/app/_assets/illustrations/skills/design-skills.svg @@ -0,0 +1,907 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/skills/devops-skills.svg b/app/_assets/illustrations/skills/devops-skills.svg new file mode 100644 index 0000000..7add317 --- /dev/null +++ b/app/_assets/illustrations/skills/devops-skills.svg @@ -0,0 +1,662 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/skills/dotnet-skills.svg b/app/_assets/illustrations/skills/dotnet-skills.svg new file mode 100644 index 0000000..471ff5f --- /dev/null +++ b/app/_assets/illustrations/skills/dotnet-skills.svg @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_assets/illustrations/skills/nodejs-skills.svg b/app/_assets/illustrations/skills/nodejs-skills.svg new file mode 100644 index 0000000..b7bfa43 --- /dev/null +++ b/app/_assets/illustrations/skills/nodejs-skills.svg @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/_components/Button.module.scss b/app/_components/Button.module.scss new file mode 100644 index 0000000..58336d7 --- /dev/null +++ b/app/_components/Button.module.scss @@ -0,0 +1,63 @@ +@import "../theme.scss"; + +.button +{ + @include formBase; + + cursor: pointer; + gap: $spacingSNudge; + justify-content: center; + text-align: left; + + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + + &.iconBefore > :first-child, + &.iconAfter > :last-child + { + font-size: $fontSizeBase600; + height: $fontSizeBase600; + width: $fontSizeBase600; + } + + &:not(.content) + { + min-width: 40px; + padding: $spacingXS; + justify-content: center; + } + + &.secondary + { + border-color: transparent; + + &:hover, + &:focus-visible + { + border: $strokeWidthThin solid $colorNeutralForeground1; + color: $colorNeutralForeground1; + } + } + + &.primary + { + background-image: linear-gradient($colorNeutralBackgroundInverted, $colorNeutralBackgroundInverted); + + &:not(:disabled, [disabled]) + { + + &:hover, + &:focus-visible + { + color: $colorNeutralForegroundInverted; + + &:active + { + background-image: linear-gradient($colorNeutralForeground3Pressed, $colorNeutralForeground3Pressed); + color: $colorNeutralForegroundInverted2; + } + } + } + } +} diff --git a/app/_components/Button.tsx b/app/_components/Button.tsx new file mode 100644 index 0000000..fb0546c --- /dev/null +++ b/app/_components/Button.tsx @@ -0,0 +1,72 @@ +import Link, { LinkProps } from "next/link"; +import React, { useMemo } from "react"; +import cls from "./Button.module.scss"; + +const Button: React.FC = ({ + as = "button", + iconAfter, + icon, + appearance = "primary", + children, + ...props +}) => +{ + const Component = as === "button" && !props.href ? + "button" : + as === "next" ? + Link : "a"; + + const classList: string = useMemo(() => + { + const list: string[] = [ cls.button, cls[appearance] ]; + + // We need these classes to differentiate content in CSS + if (icon) + list.push(cls.iconBefore); + if (iconAfter) + list.push(cls.iconAfter); + if (children) + list.push(cls.content); + + if (props.className) + list.push(props.className); + + return list.join(" "); + }, [appearance, children, icon, iconAfter, props.className]); + + return ( + + { icon } + { children } + { iconAfter } + + ); +}; + +export default Button; + +// Since we want to render button as both "a" and "button" (depending on the props) we do a little trick here +// Shorthand types +type HtmlButtonProps = Omit, "disabled">; +type HtmlAnchorProps = Omit, "type">; +type NextLinkProps = Omit, keyof LinkProps> & LinkProps & { + children?: React.ReactNode; +} & React.RefAttributes; + +type ButtonOrAnchorProps = + | ({ as?: "a"; href?: string; } & HtmlAnchorProps) // If href is present, it must be an + | ({ as?: "button"; href?: undefined; } & HtmlButtonProps) // If href is absent, it is a + + + + +); + +export default Header; diff --git a/app/_components/NavigationLinks.module.scss b/app/_components/NavigationLinks.module.scss new file mode 100644 index 0000000..883470c --- /dev/null +++ b/app/_components/NavigationLinks.module.scss @@ -0,0 +1,37 @@ +@import "../theme.scss"; + +.navigation +{ + @include flex(row); + align-items: center; + + .link + { + @include body2($fontFamilyBaseAlt); + color: inherit; + padding: $spacingSNudge $spacingM; + + @include flex(column); + + > i + { + height: $strokeWidthThick; + background-color: $colorNeutralForeground1; + border-radius: $borderRadiusSmall; + width: 0; + + transition: width $durationNormal $curveEasyEaseMax; + } + + &:hover, &:focus-visible + { + color: $colorNeutralForegroundInverted; + + > i + { + width: 100%; + background-color: $colorNeutralForegroundInverted; + } + } + } +} diff --git a/app/_components/NavigationLinks.tsx b/app/_components/NavigationLinks.tsx new file mode 100644 index 0000000..5a65169 --- /dev/null +++ b/app/_components/NavigationLinks.tsx @@ -0,0 +1,44 @@ +import links from "@/_data/links"; +import Link from "next/link"; +import React from "react"; +import cls from "./NavigationLinks.module.scss"; + +const navLinks: { text: string, href: string; }[] = + [ + { text: "Home", href: "/" }, + { text: "My skills", href: "/#skills" }, + { text: "Projects", href: "/#projects" }, + { text: "About", href: "/#about" }, + { text: "Contacts", href: "/#contacts" } + ]; + +const NavigationLinks: React.FC = ({ links: linkProps, ...props }) => ( + +); + +export default NavigationLinks; + +export type NavigationLinksProps = React.HTMLAttributes & { + links?: Omit, "href">; +}; diff --git a/app/_components/Sidemenu.module.scss b/app/_components/Sidemenu.module.scss new file mode 100644 index 0000000..a7070db --- /dev/null +++ b/app/_components/Sidemenu.module.scss @@ -0,0 +1,74 @@ +@import "../theme.scss"; + +.dialog +{ + max-height: unset; + max-width: 320px; + width: 100%; + height: 100%; + + padding: $spacingNone; + margin: $spacingNone; + margin-left: auto; + background-color: $colorNeutralBackground1; + box-shadow: $shadow16; + color: unset; + border: none; + + &::backdrop + { + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + + // Since colors use variables, and dialog is rendered outside of the regular DOM, + // we need to specify them as literals (or add variables to the dialog scope, but that's too complicated). + background-color: light-dark(rgba(255, 255, 255, 0.5), rgba(26, 26, 26, 0.5)); + } + + .wrapper + { + height: 100%; + + @include flex(column); + @include align(flex-end, center); + gap: $spacingXXXL; + padding: $spacingXXXL; + + > header + { + @include flex(row); + align-items: center; + gap: $spacingL; + + > h3 + { + @include subtitle1($fontFamilyBaseAlt); + } + } + + .navigation + { + flex-flow: column; + align-items: flex-end; + + .link + { + align-items: end; + background-position-x: 100%; + } + } + } + + transition: right $durationNormal $curveEasyEaseMax; + right: -350px; + + &.show + { + right: 0; + } +} + +body:has(dialog.menu[open]) +{ + overflow: hidden; +} diff --git a/app/_components/Sidemenu.tsx b/app/_components/Sidemenu.tsx new file mode 100644 index 0000000..60bfc1d --- /dev/null +++ b/app/_components/Sidemenu.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { Dismiss24Regular, Navigation24Regular } from "@fluentui/react-icons"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import Button, { ButtonProps } from "./Button"; +import NavigationLinks from "./NavigationLinks"; +import cls from "./Sidemenu.module.scss"; +import SocialLinks from "./SocialLinks"; +import links from "@/_data/links"; + +const Sidemenu: React.FC = ({ button, ...panelProps }) => +{ + const [isOpen, setOpen] = useState(false); + const dialogRef = useRef(null); + + const onCancel: React.ReactEventHandler = useCallback((args) => + { + args.preventDefault(); + setOpen(false); + return true; + }, []); + + // We use this method to enable user to close the menu by clicking ouside it. + const onClick: React.MouseEventHandler = useCallback((args) => + { + const wrapper = args.currentTarget.childNodes[0]; + + // If user clicked outside of the dialog boudaries, or clicked specifically on an anchor, we can close the menu + if (!wrapper.contains(args.target as Node) || args.target instanceof HTMLAnchorElement) + setOpen(false); + }, []); + + useEffect(() => + { + if (isOpen) + { + dialogRef.current?.showModal(); + } + else if (dialogRef.current?.classList.contains(cls.show)) // This check is to prevent a bug when the menu is closed before opening + { + dialogRef.current?.addEventListener("transitionend", function WaitForClose() + { + dialogRef.current?.removeEventListener("transitionend", WaitForClose); + dialogRef.current?.close(); + }); + } + + dialogRef.current?.classList.toggle(cls.show, isOpen); + }, [isOpen]); + + return <> + + + + + ; +}; + +export default Sidemenu; + +export type SidemenuProps = React.DialogHTMLAttributes & { + button?: ButtonProps; +}; diff --git a/app/_components/SocialLinks.module.scss b/app/_components/SocialLinks.module.scss new file mode 100644 index 0000000..d83f029 --- /dev/null +++ b/app/_components/SocialLinks.module.scss @@ -0,0 +1,64 @@ +@import "../theme.scss"; + +.socials +{ + @include flex(row); + align-items: center; + gap: $spacingS; + + .link + { + background-image: none; + padding: $spacingNone; + border-radius: $borderRadiusCircular; + + --bg-color: var(--network-color); + --icon-color: var(--colorNeutralForegroundStaticInverted); + + // Icon + g:first-child + { + fill: var(--icon-color) !important; + } + + // Mask + g:last-child + { + fill: var(--bg-color) !important; + } + + &:hover, + &:focus-visible + { + --icon-color: var(--network-color); + --bg-color: transparent; + + &:active + { + --bg-color: var(--colorNeutralBackground1Pressed); + } + } + + // Since GitHub has dark brand color, we need to invert it in dark mode + &.github + { + @media (prefers-color-scheme: dark) + { + --bg-color: var(--colorNeutralForegroundStaticInverted); + --icon-color: var(--network-color); + + &:hover, + &:focus-visible + { + --bg-color: transparent; + --icon-color: var(--colorNeutralForegroundStaticInverted); + + &:active + { + --bg-color: var(--colorNeutralBackground1Pressed); + } + } + } + } + } +} diff --git a/app/_components/SocialLinks.tsx b/app/_components/SocialLinks.tsx new file mode 100644 index 0000000..6058476 --- /dev/null +++ b/app/_components/SocialLinks.tsx @@ -0,0 +1,30 @@ +import socials from "@/_data/socials"; +import React from "react"; +import { SocialIcon, networkFor, social_icons } from "react-social-icons"; +import cls from "./SocialLinks.module.scss"; + +const SocialLinks: React.FC = ({ size = 50, ...props }) => ( +
+ { Object.entries(socials).map(([network, i]) => + + + + ) } +
+); + +export default SocialLinks; + +export type SocialLinksProps = React.HTMLAttributes & { + size?: number; +}; diff --git a/app/_data/FrontSection.module.scss b/app/_data/FrontSection.module.scss new file mode 100644 index 0000000..c0dfe18 --- /dev/null +++ b/app/_data/FrontSection.module.scss @@ -0,0 +1,72 @@ +@import "../theme.scss"; + +.section +{ + @include centerTwo; + min-height: 75vh; + align-items: end; + + .content + { + @include flex(column); + @include subtitle1($fontFamilyBaseAlt); + gap: $spacingXL; + + h1 + { + @include display($fontFamilyBaseAlt); + } + + h2 + { + @include title2($fontFamilyBaseAlt); + } + + .ctaButtons + { + @include flex(row, wrap); + gap: $spacingS; + } + } + + .highlight + { + color: $colorNeutralForegroundInverted; + background-color: $colorNeutralBackgroundInverted; + padding: $spacingXXS $spacingNone; + + &::selection + { + color: $colorNeutralForegroundInverted; + background-color: $colorBrandForeground1; + } + } + + .illustrations + { + justify-self: center; + position: relative; + + margin-right: 48px; + margin-bottom: 12px; + + .main + { + border-radius: $borderRadiusCircular; + max-width: 512px; + width: 100%; + height: auto; + } + + .secondary + { + position: absolute; + bottom: -12px; + left: calc(60% + 48px); + + width: 100%; + max-width: 40%; + height: auto; + } + } +} diff --git a/app/_data/FrontSection.tsx b/app/_data/FrontSection.tsx new file mode 100644 index 0000000..db924a0 --- /dev/null +++ b/app/_data/FrontSection.tsx @@ -0,0 +1,32 @@ +import { homeDecor, profilePicture } from "@/_assets/illustrations"; +import Button from "@/_components/Button"; +import Image from "next/image"; +import React from "react"; +import cls from "./FrontSection.module.scss"; +import links from "./links"; +import Package from "@/../package.json"; + +const FrontSection: React.FC = () => ( +
+
+

Hello World!

+

{ Package.author.name } is here!

+

+ I am a software engineer with extensive experience in
+ .NET and React development
+ and you are on my website! +

+
+ + +
+
+ +
+ { + { +
+
+); + +export default FrontSection; diff --git a/app/_data/ThirdPartyAttributiont.tsx b/app/_data/ThirdPartyAttributiont.tsx new file mode 100644 index 0000000..1f79854 --- /dev/null +++ b/app/_data/ThirdPartyAttributiont.tsx @@ -0,0 +1,15 @@ +import React from "react"; + +const ThirdPartyAttribution: React.FC = () => <> +

+ Iconography, colorgraphy and typography of this website are based on + Microsoft's Fluent Design System licensed under + the MIT License. +

+

+ Illustrations for GUT.Schedule and FoxTube projects use assets created + by rawpixel.com on Freepik +

+; + +export default ThirdPartyAttribution; diff --git a/app/_data/TitleLogo.module.scss b/app/_data/TitleLogo.module.scss new file mode 100644 index 0000000..2361b29 --- /dev/null +++ b/app/_data/TitleLogo.module.scss @@ -0,0 +1,40 @@ +@import "../theme.scss"; + +.title +{ + @include flex(row); + align-items: center; + + background-size: 0% $strokeWidthThickest; + background-position: 33px 33px; // Some pixel-perfect stuff + + color: $colorNeutralForeground1; + + gap: $spacingS; + padding: $spacingNone; + padding-right: $spacingS; + + > img + { + width: 36px; + height: 36px; + } + + > p + { + @include title3($fontFamilyBaseAlt); + + > sub + { + @include caption1($fontFamilyBaseAlt); + vertical-align: baseline; + } + } + + &:hover, + &:focus-visible + { + background-size: 100% $strokeWidthThickest; + color: $colorNeutralForeground1; + } +} diff --git a/app/_data/TitleLogo.tsx b/app/_data/TitleLogo.tsx new file mode 100644 index 0000000..1cb289e --- /dev/null +++ b/app/_data/TitleLogo.tsx @@ -0,0 +1,20 @@ +import Link from "next/link"; +import Image from "next/image"; +import React from "react"; +import logo from "@/icon.svg"; +import cls from "./TitleLogo.module.scss"; + +const TitleLogo: React.FC = () => ( + + A fox jumping down, and a diagonal stripe in the background, forming letters X and F +

+ xfox111 + .net +

+ +); + +export default TitleLogo; diff --git a/app/_data/bio.ts b/app/_data/bio.ts new file mode 100644 index 0000000..b47d478 --- /dev/null +++ b/app/_data/bio.ts @@ -0,0 +1,16 @@ +export const bio: string[] = + [ + "My name is Eugene Fox. I am a professional software developer primarily focused on .NET and React projects.", + + "My journey as a programmer started in 2018 from a silly free-time hobby. Since then I've released a couple of personal projects, some of which have become quite popular.", + + "Graduated from Bonch-Bruevich University of Telecommunications in 2023 where I've got my Bachelor degree in Computer science. It was fun. Took part in a number of hackathons (usually 1st place for us) as well as science conferences (including those, hosted by IEEE).", + + "Also before graduation I've managed to work in several different companies in different IT fields (mostly software development, of course).", + + "Out-of-box thinking and constant self-improvement is my life strategy. New tool released? - Yes, please! GitHub is hosting another conference? - Sign me up! There's a new challenging task to complete? - Oh, boy, here we go again! So much things to learn, so little time to spare...", + + "Overall, enthusiastic, fast learning and energetic person. Love coding and creating something new. Like to draw and compose music. Proud member of the furry community." + ]; + +export default bio; diff --git a/app/_data/contacts.ts b/app/_data/contacts.ts new file mode 100644 index 0000000..023d983 --- /dev/null +++ b/app/_data/contacts.ts @@ -0,0 +1,45 @@ +import socials, { Socials } from "./socials"; +import Package from "@/../package.json"; + +const contacts: ContactLinks = +{ + email: + { + text: Package.author.email, + href: "mailto:" + Package.author.email + }, + telephone: + { + text: "+7 996 929-19-69", + href: "tel:79969291969", + country: "Russia" + }, + socials: + { + "LinkedIn": socials["LinkedIn"], + "Telegram": + { + username: "@xfox111", + href: "https://t.me/xfox111" + }, + "Twitter": socials["Twitter"] + } +}; + +export default contacts; + +export type ContactLinks = + { + email: + { + text: string; + href: string; + }; + telephone?: + { + text: string; + href: string; + country: string; + }; + socials: Socials; + }; diff --git a/app/_data/experience.ts b/app/_data/experience.ts new file mode 100644 index 0000000..395ab88 --- /dev/null +++ b/app/_data/experience.ts @@ -0,0 +1,20 @@ +const experience: WorkplaceEntry[] = + [ + { title: "IT/VR tutor", year: "2020", place: "Quantorium", tech: "Unity, STEM" }, + { title: "System administrator", year: "2021", place: "Quantorium", tech: "M365, Intune, Azure" }, + { title: "Software Engineer", place: "[nordcloud]", tech: "ASP.NET, EF Core" }, + { title: "CTO", year: "2022", place: "FoxDev Studio", tech: "Unity, Xamarin, .NET, React, Azure" }, + { title: "Software Engineer", year: "2023", place: "A-rial", tech: ".NET, React" }, + { title: "Lead Software Engineer", year: "2024 ", place: "Ubitel", tech: ".NET, React, IoT" }, + { title: "Here", place: "Your company" }, + ]; + +export default experience; + +export type WorkplaceEntry = + { + year?: string; + place?: string; + title: string; + tech?: string; + }; diff --git a/app/_data/links.ts b/app/_data/links.ts new file mode 100644 index 0000000..a70c028 --- /dev/null +++ b/app/_data/links.ts @@ -0,0 +1,19 @@ +import socials from "./socials"; + +const links: Links = +{ + blog: "https://blog.xfox111.net", + linkedin: socials["LinkedIn"].href, + resume: "/resume", + github: socials["GitHub"].href +}; + +export default links; + +type Links = +{ + blog?: string; + linkedin?: string; + resume: string; + github: string; +} diff --git a/app/_data/metadata.ts b/app/_data/metadata.ts new file mode 100644 index 0000000..ab871e3 --- /dev/null +++ b/app/_data/metadata.ts @@ -0,0 +1,48 @@ +import { Metadata } from "next"; +import bio from "./bio"; +import socials from "./socials"; +import Package from "@/../package.json"; + +export const canonicalName: URL = new URL("https://xfox111.net"); +const baseTitle: string = "Eugene Fox - Software developer"; + +const gender: string = "male"; +const keywords: string[] = ["Eugene Fox", "software developer", ".net", "react", "frontend developer", "backend developer", ".net developer", "react developer", "fullstack developer", "software engineer", "Michael Gordeev", "Mikhail Gordeev"]; + +export const getTitle = (pageTitle: string, customBase?: string): string => + pageTitle + " - " + (customBase ?? baseTitle); + +export const metadata: Metadata = +{ + title: baseTitle, + description: bio[0], + metadataBase: canonicalName, + openGraph: + { + title: baseTitle, + description: bio[0], + type: "profile", + firstName: Package.author.name.split(" ")[0], + lastName: Package.author.name.split(" ")[1], + gender, + username: socials["Twitter"].username, + siteName: canonicalName.hostname, + locale: "en_US" + }, + twitter: + { + site: socials["Twitter"].username, + card: "summary_large_image" + }, + alternates: + { + canonical: canonicalName.href + }, + authors: [ + { + name: Package.author.name, + url: socials["LinkedIn"].href + } + ], + keywords +}; diff --git a/app/_data/projects.ts b/app/_data/projects.ts new file mode 100644 index 0000000..9627326 --- /dev/null +++ b/app/_data/projects.ts @@ -0,0 +1,181 @@ +import ezlogImg from "@/_assets/illustrations/projects/EasyLogon.svg"; +import foxTubeDark from "@/_assets/illustrations/projects/FoxTube/FoxTube-dark.webp"; +import foxTubeLight from "@/_assets/illustrations/projects/FoxTube/FoxTube-light.webp"; +import gutScheduleImg from "@/_assets/illustrations/projects/GUTSchedule.svg"; +import motionDecoderDark from "@/_assets/illustrations/projects/MotionDecoder/MotionDecoder-dark.webp"; +import motionDecoderLight from "@/_assets/illustrations/projects/MotionDecoder/MotionDecoder-light.webp"; +import passwordGeneratorDark from "@/_assets/illustrations/projects/PasswordGenerator/PasswordGeneratorExtension-dark.webp"; +import passwordGeneratorLight from "@/_assets/illustrations/projects/PasswordGenerator/PasswordGeneratorExtension-light.webp"; +import simpleOtpImg from "@/_assets/illustrations/projects/SimpleOTP.svg"; +import tabsAsideDark from "@/_assets/illustrations/projects/TabsAside/dark.webp"; +import tabsAsideLight from "@/_assets/illustrations/projects/TabsAside/light.webp"; +import * as ic from "@fluentui/react-icons"; +import { StaticImageData } from "next/image"; + +const projects: Project[] = + [ + { + title: "EasyLogon", + subtitle: "QR code authentication on any website", + description: + [ + "During one of the classes at university I struggled to log into my account on a lab computer. I have long random passwords, so I had to type it in manually from my phone which took quite some time. I thought that there must be a better way to do this.", + "So I came up with this idea where you can easily send your credentials to any computer by simply scanning a QR code with a password manager app.", + "After testing it out I tried to make a startup out of it but sadly it didn't work. Still use it ocasiounally though." + ], + image: ezlogImg, + link: "https://ezlog.app/about", + stack: + [ + { text: "C#/TypeScript", icon: ic.Code24Regular }, + { text: ".NET 6", icon: ic.Server24Regular }, + { text: "ReactJS", icon: ic.PhoneDesktop24Regular }, + { text: "Xamarin.Forms", icon: ic.Phone24Regular }, + { text: "SQL Server", icon: ic.Database24Regular }, + { text: "Azure DevOps", icon: ic.Branch24Regular }, + { text: "Azure Pipelines/AppCenter", icon: ic.FlashFlow24Regular }, + { text: "AppCenter", icon: ic.HeartPulse24Regular } + ] + }, + { + title: "Tabs aside", + subtitle: "Browser extension inspired by Edge's \"Tabs aside\" and Collections features", + description: + [ + "Initially built on pure JS/CSS this extension was designed to recreate \"Tabs aside\" feature that Microsoft introduced in their EdgeHTML-based Microsoft Edge browser, but removed it in subsequent Chromium-based version.", + "Later it was rewritten in ReactJS and TypeScript and got new and unique features, yet still maintaining that original asthetics." + ], + image: tabsAsideLight, + imageDark: tabsAsideDark, + link: "https://github.com/xfox111/TabsAsideExtension", + stack: + [ + { text: "ReactJS", icon: ic.Desktop24Regular }, + { text: "TypeScript/SASS", icon: ic.Code24Regular }, + { text: "Chrome/WebExt", icon: ic.FlashSettings24Regular }, + { text: "Fluent UI", icon: ic.Color24Regular }, + { text: "GitHub", icon: ic.Branch24Regular }, + { text: "GitHub Actions", icon: ic.FlashFlow24Regular }, + ] + }, + { + title: "SimpleOTP", + subtitle: "Lightweight and simple .NET library for OTP implementation", + description: + [ + "Initially created during EasyLogon development, this library was designed as a simple, yet flexible solution for one-time password authenticators and validators.", + "It provides extensive toolset for generation, validation and management of OTP configurations and can be used in any .NET application whether it is an authenticator app or a website that accepts OTP codes." + ], + image: simpleOtpImg, + link: "https://github.com/xfox111/SimpleOTP", + stack: + [ + { text: ".NET/C#", icon: ic.Code24Regular }, + { text: "MSTest", icon: ic.Beaker24Regular }, + { text: "GitHub", icon: ic.Branch24Regular }, + { text: "GitHub Actions", icon: ic.FlashFlow24Regular }, + ] + }, + { + title: "Password generator", + subtitle: "Simple browser extension for generating passwords", + description: + [ + "Small pet project that I developed while my favorite password generator website was down.", + "Basically a playground, where I try out new technologies and approaches to web development." + ], + image: passwordGeneratorLight, + imageDark: passwordGeneratorDark, + link: "https://github.com/xfox111/PasswordGeneratorExtension", + stack: + [ + { text: "React/Vite", icon: ic.Desktop24Regular }, + { text: "TypeScript", icon: ic.Code24Regular }, + { text: "Chrome/WebExt", icon: ic.FlashSettings24Regular }, + { text: "Fluent UI", icon: ic.Color24Regular }, + { text: "GitHub", icon: ic.Branch24Regular }, + { text: "GitHub Actions", icon: ic.FlashFlow24Regular }, + ] + }, + { + title: "GUT.Schedule", + subtitle: "Mobile app that exports Bonch university schedule to e-calendar", + description: + [ + "[2019]", + "I created this app during my time in Bonch-Bruevich University of Telecommunications as a BS student.", + "It was designed to help students to manage their timetable in a more convenient and effective way." + ], + image: gutScheduleImg, + link: "https://github.com/xfox111/GUTSchedule", + stack: + [ + { text: ".NET/C#", icon: ic.Code24Regular }, + { text: "Xamarin.Android", icon: ic.Phone24Regular }, + { text: "GitHub", icon: ic.Branch24Regular }, + { text: "NUnit 3", icon: ic.Beaker24Regular }, + { text: "Azure Pipelines", icon: ic.FlashFlow24Regular }, + ] + }, + { + title: "FoxTube", + subtitle: "UWP app that gives YouTube a fresh look on Windows", + description: + [ + "[2019]", + "My first published app.", + "I like to watch videos while working on my projects, but at the time YouTube didn't have a proper picture-in-picture mode and overall had a lot of issues with the UX, so this was my way to fix this.", + "Unfortunately, Google doesn't like third-party YouTube clients." + ], + image: foxTubeLight, + imageDark: foxTubeDark, + link: "https://www.youtube.com/watch?v=Mio9FbxmbhM", + stack: + [ + { text: ".NET/C#", icon: ic.Code24Regular }, + { text: "UWP", icon: ic.Desktop24Regular }, + { text: "Azure DevOps", icon: ic.Branch24Regular }, + { text: "AppCenter", icon: ic.HeartPulse24Regular }, + { text: "Azure Pipelines", icon: ic.FlashFlow24Regular }, + ] + }, + { + title: "MotionDecoder", + subtitle: "CCTV footage analysis tool", + description: + [ + "[2018]", + "My earliest attempt in software development.", + "Basically this program analyzes pre-recorded CCTV footage by comparing different frames and using some simple algorithms and provides user with a set of timecodes where a motion was detected.", + ], + image: motionDecoderLight, + imageDark: motionDecoderDark, + link: "https://github.com/xfox111/MotionDecoder", + stack: + [ + { text: ".NET/C#", icon: ic.Code24Regular }, + { text: "WinForms", icon: ic.Desktop24Regular }, + { text: "Accord.NET", icon: ic.FlashSettings24Regular }, + { text: "GitHub", icon: ic.Branch24Regular }, + ] + } + ]; + +export default projects; + +export type Project = + { + title: string; + subtitle: string; + description: string[]; + image: string | StaticImageData; + imageDark?: string | StaticImageData; + stack: TechStackItem[]; + link: string; + }; + +type TechStackItem = + { + icon: ic.FluentIcon; + text: string; + }; diff --git a/app/_data/resumeList.ts b/app/_data/resumeList.ts new file mode 100644 index 0000000..abe65f1 --- /dev/null +++ b/app/_data/resumeList.ts @@ -0,0 +1,40 @@ +import { ImageExport } from "@/_assets/assets"; +import { dotnetResume, fullstackResume, reactResume } from "@/_assets/illustrations"; + +const resumeList: Resume[] = + [ + { + key: "dotnet", + label: ".NET developer", + pageIndex: 1, + fileName: "Resume - Eugene Fox - .NET developer", + image: dotnetResume + }, + { + key: "react", + label: "React developer", + pageIndex: 0, + fileName: "Resume - Eugene Fox - React developer", + image: reactResume + }, + { + key: "fullstack", + label: "Fullstack developer", + pageIndex: 2, + fileName: "Resume - Eugene Fox - Fullstack developer", + image: fullstackResume, + default: true + }, + ]; + +export default resumeList; + +export type Resume = + { + key: string; + pageIndex: number; + label: string; + image: ImageExport; + fileName: string; + default?: true; + }; diff --git a/app/_data/skills.ts b/app/_data/skills.ts new file mode 100644 index 0000000..9cb75bb --- /dev/null +++ b/app/_data/skills.ts @@ -0,0 +1,60 @@ +import { ImageExport } from "@/_assets/assets"; +import imgs from "@/_assets/illustrations/skills"; +import * as ic from "@fluentui/react-icons"; + +const skills: Skill[] = + [ + { + title: "NodeJS", + description: "React, Vite, Next.js, SASS, TypeScript", + icon: ic.WindowDevToolsRegular, + image: imgs.nodejs + }, + { + title: ".NET", + description: "ASP.NET, Razor, WinUI/UWP, WPF, WinForms | Xamarin.Forms, MAUI", + icon: ic.PhoneDesktopRegular, + image: imgs.dotnet + }, + { + title: "Architecture & systems", + description: "Docker, Nginx, Linux | Modules, microservices", + icon: ic.DesktopFlowRegular, + image: imgs.architecture + }, + { + title: "Databases", + description: "Entity Framework, MongoDB", + icon: ic.DatabaseMultipleRegular, + image: imgs.databases + }, + { + title: "Design", + description: "Figma, Photoshop, Illustrator", + icon: ic.DesignIdeasRegular, + // Note, this picture has a special behavior in @/_page_sections/SkillsSection.tsx:24 + image: imgs.design + }, + { + title: "DevOps", + description: "GitHub, Azure DevOps, AppCenter, Atlassian", + icon: ic.FlashFlowRegular, + image: imgs.devops + }, + { + title: "Administration", + description: "Ansible, M365, Azure, InTune", + icon: ic.ConnectedRegular, + image: imgs.admin + } + ]; + +export default skills; + +export type Skill = + { + title: string; + description: string; + icon: ic.FluentIcon; + image: ImageExport; + }; diff --git a/app/_data/socials.ts b/app/_data/socials.ts new file mode 100644 index 0000000..a2f1b25 --- /dev/null +++ b/app/_data/socials.ts @@ -0,0 +1,30 @@ +import Package from "@/../package.json"; + +const socials: Socials = + { + "GitHub": + { + href: Package.author.url, + username: "@xfox111" + }, + "LinkedIn": + { + href: "https://www.linkedin.com/in/xfox/", + username: "@xfox" + }, + "Twitter": + { + href: "https://twitter.com/xfox111", + username: "@xfox111" + }, + }; + +export default socials; + +export type Socials = Record; + +export type SocialLink = + { + href: string; + username: string; + }; diff --git a/app/_page_sections/AboutSection.module.scss b/app/_page_sections/AboutSection.module.scss new file mode 100644 index 0000000..a6a9edc --- /dev/null +++ b/app/_page_sections/AboutSection.module.scss @@ -0,0 +1,26 @@ +@import "../theme.scss"; + +.section +{ + @include centerTwo; + @include body2($fontFamilyBaseAlt); + color: $colorNeutralForeground2; + align-items: center; + + > div:first-child + { + @include flex(column); + gap: $spacingM; + } + + > img + { + height: auto; + width: 100%; + max-width: 400px; + justify-self: center; + + border-radius: $borderRadiusMedium; + box-shadow: $shadow2; + } +} diff --git a/app/_page_sections/AboutSection.tsx b/app/_page_sections/AboutSection.tsx new file mode 100644 index 0000000..fc55e7b --- /dev/null +++ b/app/_page_sections/AboutSection.tsx @@ -0,0 +1,21 @@ +import { aboutPicture } from "@/_assets/illustrations"; +import bio from "@/_data/bio"; +import Image from "next/image"; +import React from "react"; +import cls from "./AboutSection.module.scss"; + +const AboutSection: React.FC = () => ( +
+
+

About me

+ + { bio.map((i, index) => +

{ i }

+ ) } +
+ + { +
+); + +export default AboutSection; diff --git a/app/_page_sections/ContactSection.module.scss b/app/_page_sections/ContactSection.module.scss new file mode 100644 index 0000000..a3f5615 --- /dev/null +++ b/app/_page_sections/ContactSection.module.scss @@ -0,0 +1,91 @@ +@import "../theme.scss"; + +.section +{ + @include flex(column); + gap: $spacingXXXL; + + @include body1; + + h2 + { + text-align: center; + } + + .content + { + @include centerTwo; + + .container + { + @include flex(column); + gap: $spacingM; + } + + .contacts + { + align-items: flex-end; + text-align: right; + } + + .textarea + { + min-width: 100%; + min-height: 160px; + max-width: 60vw; + resize: both; + } + + .formToolbar + { + @include flex(row); + justify-content: flex-end; + + @media screen and (max-width: 460px) + { + flex-flow: column; + row-gap: $spacingM; + align-items: flex-end; + + .status > span + { + @include body1($fontFamilyBaseAlt); + } + } + + .status + { + @include flex(row); + @include align(center, flex-end); + @include body2($fontFamilyBaseAlt); + + height: 40px; + width: 0; + overflow: hidden; + + > span + { + margin: $spacingS $spacingM; + text-wrap: nowrap; + } + + color: $colorNeutralForegroundStaticInverted; + background-color: $colorStatusDangerBackground3; + + transition-property: width; + transition-duration: $durationNormal; + transition-timing-function: $curveEasyEaseMax; + + &:is(.error, .success) + { + width: 100%; + } + + &.success + { + background-color: $colorStatusSuccessBackground3; + } + } + } + } +} diff --git a/app/_page_sections/ContactSection.tsx b/app/_page_sections/ContactSection.tsx new file mode 100644 index 0000000..f3e447c --- /dev/null +++ b/app/_page_sections/ContactSection.tsx @@ -0,0 +1,118 @@ +"use client"; + +import Button from "@/_components/Button"; +import contacts from "@/_data/contacts"; +import FormStatusTracker from "@/_utils/FormStatusTracker"; +import React, { InputHTMLAttributes, useMemo, useState } from "react"; +import { useFormState } from "react-dom"; +import sendInquiry, { FormStatus } from "../_utils/sendInquiry"; +import cls from "./ContactSection.module.scss"; + +const defaultState: FormStatus = { status: "idle" }; + +const ContactSection: React.FC = () => +{ + const [pending, setPending] = useState(false); + const [{ status, message }, formAction] = useFormState(sendInquiry, defaultState); + const { telephone: phone, email, socials } = contacts; + + const sharedProps: InputHTMLAttributes = useMemo(() => ({ + required: true, + disabled: pending, + readOnly: status === "success" + }), [status, pending]); + + return ( +
+

Let's get in touch

+ +
+ +
+

Inquiries, requests or proposals

+ +
+ + + + + + +