feat: stricter lint, check pipelines, docker-containers, pnpm
This commit is contained in:
@@ -28,6 +28,87 @@
|
|||||||
"error",
|
"error",
|
||||||
"double"
|
"double"
|
||||||
],
|
],
|
||||||
"semi": "error"
|
"semi": "error",
|
||||||
|
"no-console": "warn",
|
||||||
|
"no-debugger": "error",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
|
"@typescript-eslint/explicit-function-return-type": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowExpressions": true,
|
||||||
|
"allowTypedFunctionExpressions": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "error",
|
||||||
|
"@typescript-eslint/strict-boolean-expressions": "off",
|
||||||
|
"@typescript-eslint/naming-convention": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"selector": "interface",
|
||||||
|
"format": ["PascalCase"],
|
||||||
|
"prefix": ["I"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "typeAlias",
|
||||||
|
"format": ["PascalCase"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
|
"no-var": "error",
|
||||||
|
"prefer-const": "error",
|
||||||
|
"prefer-arrow-callback": "error",
|
||||||
|
"no-trailing-spaces": "error",
|
||||||
|
"eol-last": ["error", "always"],
|
||||||
|
"object-curly-spacing": ["error", "always"],
|
||||||
|
"array-bracket-spacing": ["error", "never"],
|
||||||
|
"comma-dangle": ["error", "never"],
|
||||||
|
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }],
|
||||||
|
"arrow-spacing": ["error", { "before": true, "after": true }],
|
||||||
|
"keyword-spacing": ["error", { "before": true, "after": true }],
|
||||||
|
"max-len": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"code": 120,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"ignoreUrls": true,
|
||||||
|
"ignoreStrings": true,
|
||||||
|
"ignoreTemplateLiterals": true,
|
||||||
|
"ignoreRegExpLiterals": true,
|
||||||
|
"ignoreComments": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max-lines": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"max": 300,
|
||||||
|
"skipBlankLines": true,
|
||||||
|
"skipComments": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max-lines-per-function": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"max": 50,
|
||||||
|
"skipBlankLines": true,
|
||||||
|
"skipComments": true,
|
||||||
|
"IIFEs": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"complexity": ["error", 10],
|
||||||
|
"max-depth": ["error", 4],
|
||||||
|
"max-params": ["error", 4],
|
||||||
|
"max-nested-callbacks": ["error", 3],
|
||||||
|
"max-statements": ["error", 20, { "ignoreTopLevelFunctions": false }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
.github/workflows/pr-lint-check.yml
vendored
Normal file
41
.github/workflows/pr-lint-check.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: PR Lint Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Run ESLint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Comment PR on failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.issues.createComment({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: '❌ ESLint check failed. Please fix the linting errors before merging.'
|
||||||
|
})
|
||||||
35
.github/workflows/push-lint-check.yml
vendored
Normal file
35
.github/workflows/push-lint-check.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Push Lint Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
- '**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Run ESLint on Push
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Notify on failure
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
echo "::error::ESLint check failed. Please run 'npm run lintfix' locally to fix issues."
|
||||||
73
.github/workflows/security-audit.yml
vendored
Normal file
73
.github/workflows/security-audit.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
name: Security Audit
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 1'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security-audit:
|
||||||
|
name: Security Vulnerability Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run pnpm audit
|
||||||
|
run: pnpm audit --audit-level=moderate
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Generate security report
|
||||||
|
run: pnpm run security:check
|
||||||
|
|
||||||
|
- name: Check for outdated dependencies
|
||||||
|
run: pnpm outdated || true
|
||||||
|
|
||||||
|
- name: Upload security report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: security-report
|
||||||
|
path: security-report.json
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Notify on high/critical vulnerabilities
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
echo "::error::Security vulnerabilities detected! Check the security report artifact for details."
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -4,6 +4,8 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
.pnpm-store
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
@@ -33,3 +35,10 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# security
|
||||||
|
security-report.json
|
||||||
|
*.audit.json
|
||||||
|
|
||||||
|
# docker
|
||||||
|
.dockerignore
|
||||||
|
|||||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
18
|
||||||
370
AGENTS.md
Normal file
370
AGENTS.md
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
# Portfolio Project Development Rules
|
||||||
|
|
||||||
|
## Code Style & Formatting
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
- **Tab indentation** (not spaces), **double quotes**, **semicolons** required
|
||||||
|
- **Explicit function return types** for all functions (except inline expressions)
|
||||||
|
- **Never use `any` type** - always provide proper typing
|
||||||
|
- Prefix **interfaces with `I`** (e.g., `IProps`), **PascalCase** for type aliases
|
||||||
|
- Underscore prefix for unused variables (e.g., `_unusedParam`)
|
||||||
|
- Avoid non-null assertions (`!`) - use proper null checks
|
||||||
|
|
||||||
|
### React & JSX
|
||||||
|
- Components return `JSX.Element`, functional components with hooks only
|
||||||
|
- Self-closing tags for components without children: `<Intro/>`
|
||||||
|
- No prop-types (using TypeScript)
|
||||||
|
|
||||||
|
### Spacing & Formatting
|
||||||
|
- **Object curly spacing**: `{ foo }`, **no array bracket spacing**: `[1, 2]`
|
||||||
|
- **No trailing commas**, arrow spacing: `() => {}`
|
||||||
|
- Max **1 empty line** between blocks, end files with newline, no trailing spaces
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
- Use path alias `@/*` (configured in tsconfig)
|
||||||
|
- Group: external deps first, then internal modules
|
||||||
|
- Import from `@/src/portfolio/...` and `@/src/styles/...`
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### File Organization
|
||||||
|
- **Pages**: `/pages/*.tsx` (pages router, not app router)
|
||||||
|
- **Components**: `/src/portfolio/sections/*.tsx`
|
||||||
|
- **Styles**: `/src/styles/*.scss` with helpers
|
||||||
|
- **Data**: `/src/portfolio/data/`, **Helpers**: `/src/portfolio/helpers/`
|
||||||
|
- **Public**: `/public/` (PDFs, images, icons)
|
||||||
|
|
||||||
|
### Component Structure
|
||||||
|
- Section components in `/src/portfolio/sections/`, main page in `/src/portfolio/MainPage.tsx`
|
||||||
|
- Each section has single responsibility
|
||||||
|
|
||||||
|
## Styling & Libraries
|
||||||
|
|
||||||
|
### SCSS
|
||||||
|
- Import helpers: `@import "helpers";`, use variables: `$accent_colour`
|
||||||
|
- BEM-like naming, `!important` only for library overrides (Splide)
|
||||||
|
|
||||||
|
### Animations
|
||||||
|
- **AOS** (scroll), **Anime.js** (complex), **react-transition-group** (transitions)
|
||||||
|
- Respect `prefers-reduced-motion`
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
- **Next.js 15** (pages router), **React 18.2**, **TypeScript 5.1**, **SASS**
|
||||||
|
- **@splidejs/react-splide**, **react-icons**, **Sharp**
|
||||||
|
- Dev server on **port 4000**: `npm run dev`
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
- `npm run dev` - Start dev server (port 4000)
|
||||||
|
- `npm run lintfix` - Auto-fix linting issues
|
||||||
|
- `npm run lint` - Check for issues
|
||||||
|
- `npm run build` - Production build
|
||||||
|
|
||||||
|
### Windsurf Commands Helper Script
|
||||||
|
The project includes `windsurf-commands.sh` for common development tasks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./windsurf-commands.sh lint # Run ESLint to check for code issues
|
||||||
|
./windsurf-commands.sh lintfix # Run ESLint with auto-fix enabled
|
||||||
|
./windsurf-commands.sh help # Show available commands
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage in AGENTS.md:**
|
||||||
|
- When referencing linting in rules, can use either `npm run lint` or `./windsurf-commands.sh lint`
|
||||||
|
- Script provides consistent interface for development commands
|
||||||
|
- Includes error handling and user-friendly output
|
||||||
|
|
||||||
|
### Before Committing
|
||||||
|
1. Run `npm run lintfix` and `npm run lint`
|
||||||
|
2. Ensure TypeScript compiles
|
||||||
|
3. Test changes in browser
|
||||||
|
|
||||||
|
## File Naming
|
||||||
|
- **Components**: PascalCase (`MainPage.tsx`)
|
||||||
|
- **Utilities**: camelCase (`helpers.ts`)
|
||||||
|
- **Styles**: kebab-case/camelCase (`main.scss`)
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Component
|
||||||
|
const ComponentName = (): JSX.Element => {
|
||||||
|
return <div>{/* content */}</div>;
|
||||||
|
};
|
||||||
|
export default ComponentName;
|
||||||
|
|
||||||
|
// Interface
|
||||||
|
interface IComponentProps {
|
||||||
|
title: string;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```scss
|
||||||
|
// SCSS
|
||||||
|
@import "helpers";
|
||||||
|
.component { color: $accent_colour; }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Complexity Constraints
|
||||||
|
|
||||||
|
### Strict Limits (ESLint Enforced)
|
||||||
|
- **Max line length**: 120 characters (ignores URLs, strings, template literals)
|
||||||
|
- **Max file length**: 300 lines (excluding blank lines and comments)
|
||||||
|
- **Max function length**: 50 lines (excluding blank lines and comments)
|
||||||
|
- **Max cyclomatic complexity**: 10 per function
|
||||||
|
- **Max nesting depth**: 4 levels
|
||||||
|
- **Max parameters**: 4 per function
|
||||||
|
- **Max nested callbacks**: 3 levels
|
||||||
|
- **Max statements**: 20 per function
|
||||||
|
|
||||||
|
### Refactoring Strategies
|
||||||
|
|
||||||
|
When files exceed limits, use these approaches:
|
||||||
|
|
||||||
|
1. **Split by logical sections**: Break large files into feature folders with main component, sub-components, helpers, types, and index.ts
|
||||||
|
2. **Extract helper functions**: Move utility functions to separate `helpers.ts` files
|
||||||
|
3. **Extract types**: Move interfaces/types to separate `types.ts` files
|
||||||
|
4. **Break down complex functions**: Use sub-functions, early returns, extract conditionals, use lookup objects
|
||||||
|
|
||||||
|
**Folder structure example:**
|
||||||
|
```
|
||||||
|
/src/portfolio/sections/Experience/
|
||||||
|
Experience.tsx, ExperienceCard.tsx, helpers.ts, types.ts, index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Handling violations:**
|
||||||
|
1. Analyze what limit was exceeded
|
||||||
|
2. Choose appropriate strategy
|
||||||
|
3. Plan folder structure
|
||||||
|
4. Execute incrementally (create files, move code, update imports, test)
|
||||||
|
5. Verify with `npm run lint`
|
||||||
|
|
||||||
|
**Line length fixes:** Break strings, split chains, extract variables, reformat JSX
|
||||||
|
**Parameter count fixes:** Use object parameters, configuration objects, or split function
|
||||||
|
|
||||||
|
## QA and Testing
|
||||||
|
|
||||||
|
### Pre-Testing Checklist
|
||||||
|
|
||||||
|
Before running any tests, verify the development server is running:
|
||||||
|
|
||||||
|
1. **Check if dev server is running** using `lsof`:
|
||||||
|
```bash
|
||||||
|
lsof -i :4000
|
||||||
|
```
|
||||||
|
- If output shows a process, server is running
|
||||||
|
- If no output, start the server with `npm run dev`
|
||||||
|
|
||||||
|
2. **Verify server accessibility**:
|
||||||
|
```bash
|
||||||
|
curl -I http://localhost:4000
|
||||||
|
```
|
||||||
|
- Should return HTTP 200 OK
|
||||||
|
- If connection refused, server needs to be started
|
||||||
|
|
||||||
|
### Playwright MCP Testing Workflow
|
||||||
|
|
||||||
|
When testing changes or new features, follow this systematic approach:
|
||||||
|
|
||||||
|
#### Step 1: Environment Setup
|
||||||
|
1. Ensure dev server is running on port 4000
|
||||||
|
2. Install Playwright browsers if needed (use `mcp0_browser_install` if errors occur)
|
||||||
|
3. Navigate to `http://localhost:4000`
|
||||||
|
|
||||||
|
#### Step 2: Visual Testing
|
||||||
|
Use Playwright MCP to verify visual elements:
|
||||||
|
|
||||||
|
1. **Take accessibility snapshot** (`mcp0_browser_snapshot`):
|
||||||
|
- Captures page structure and content
|
||||||
|
- Better than screenshots for testing
|
||||||
|
- Use this to verify all sections are present
|
||||||
|
|
||||||
|
2. **Check key sections are visible**:
|
||||||
|
- Intro section with name/title
|
||||||
|
- Achievements section
|
||||||
|
- Experience section with carousel
|
||||||
|
- Projects section
|
||||||
|
- Skills and Links section
|
||||||
|
- Footer
|
||||||
|
|
||||||
|
3. **Verify responsive behavior**:
|
||||||
|
- Test at different viewport sizes using `mcp0_browser_resize`
|
||||||
|
- Common breakpoints: 375px (mobile), 768px (tablet), 1920px (desktop)
|
||||||
|
- Check that content adapts properly
|
||||||
|
|
||||||
|
#### Step 3: Functional Testing
|
||||||
|
Test interactive elements:
|
||||||
|
|
||||||
|
1. **Navigation and scrolling**:
|
||||||
|
- Verify smooth scroll behavior (if `prefers-reduced-motion: no-preference`)
|
||||||
|
- Test anchor links if present
|
||||||
|
- Check scroll-triggered animations (AOS)
|
||||||
|
|
||||||
|
2. **Carousel functionality** (Splide):
|
||||||
|
- Click next/previous arrows using `mcp0_browser_click`
|
||||||
|
- Verify slides change correctly
|
||||||
|
- Check pagination dots are clickable
|
||||||
|
- Ensure carousel is responsive
|
||||||
|
|
||||||
|
3. **Interactive elements**:
|
||||||
|
- Test any buttons, links, or accordions
|
||||||
|
- Verify hover states work
|
||||||
|
- Check click handlers fire correctly
|
||||||
|
- Test form inputs if present
|
||||||
|
|
||||||
|
4. **External links**:
|
||||||
|
- Verify PDF links open correctly (CV, research papers)
|
||||||
|
- Check social media links
|
||||||
|
- Ensure `target="_blank"` and `rel="noopener noreferrer"` where needed
|
||||||
|
|
||||||
|
#### Step 4: Accessibility Testing
|
||||||
|
|
||||||
|
1. **Contrast ratios**:
|
||||||
|
- Use `mcp0_browser_evaluate` to check computed styles
|
||||||
|
- Verify text has sufficient contrast against backgrounds
|
||||||
|
- Check accent color (`$accent_colour`) meets WCAG AA standards (4.5:1 for normal text)
|
||||||
|
|
||||||
|
2. **Keyboard navigation**:
|
||||||
|
- Use `mcp0_browser_press_key` to test Tab navigation
|
||||||
|
- Verify focus indicators are visible
|
||||||
|
- Check all interactive elements are keyboard accessible
|
||||||
|
- Test Escape key for modals/overlays
|
||||||
|
|
||||||
|
3. **ARIA labels and semantic HTML**:
|
||||||
|
- Check snapshot for proper heading hierarchy (h1, h2, h3)
|
||||||
|
- Verify buttons have accessible names
|
||||||
|
- Check images have alt text
|
||||||
|
- Ensure landmarks are properly labeled
|
||||||
|
|
||||||
|
4. **Screen reader compatibility**:
|
||||||
|
- Verify meaningful content order in accessibility snapshot
|
||||||
|
- Check that hidden decorative elements are properly hidden
|
||||||
|
- Ensure dynamic content updates are announced
|
||||||
|
|
||||||
|
#### Step 5: Performance Checks
|
||||||
|
|
||||||
|
1. **Console errors**:
|
||||||
|
- Use `mcp0_browser_console_messages` to check for errors
|
||||||
|
- Verify no React warnings in development
|
||||||
|
- Check for missing image errors
|
||||||
|
- Look for failed network requests
|
||||||
|
|
||||||
|
2. **Network requests**:
|
||||||
|
- Use `mcp0_browser_network_requests` to inspect requests
|
||||||
|
- Verify images are optimized (using Next.js Image)
|
||||||
|
- Check for unnecessary requests
|
||||||
|
- Ensure no 404s or failed requests
|
||||||
|
|
||||||
|
3. **Animation performance**:
|
||||||
|
- Verify AOS animations trigger smoothly
|
||||||
|
- Check Anime.js animations don't cause jank
|
||||||
|
- Test on reduced motion preference
|
||||||
|
|
||||||
|
#### Step 6: Cross-Section Integration
|
||||||
|
|
||||||
|
1. **Section transitions**:
|
||||||
|
- Scroll through all sections
|
||||||
|
- Verify spacing between sections is consistent
|
||||||
|
- Check that animations don't overlap or conflict
|
||||||
|
|
||||||
|
2. **Data consistency**:
|
||||||
|
- Verify project data displays correctly
|
||||||
|
- Check experience timeline is accurate
|
||||||
|
- Ensure skills list is complete and categorized
|
||||||
|
|
||||||
|
3. **Asset loading**:
|
||||||
|
- Verify all images load (profile, project screenshots)
|
||||||
|
- Check PDFs are accessible
|
||||||
|
- Ensure icons render correctly (react-icons)
|
||||||
|
|
||||||
|
### Testing Checklist Template
|
||||||
|
|
||||||
|
When testing a new feature or change, use this checklist:
|
||||||
|
|
||||||
|
- [ ] Dev server running on port 4000
|
||||||
|
- [ ] Page loads without errors (check console)
|
||||||
|
- [ ] All sections render correctly (accessibility snapshot)
|
||||||
|
- [ ] Responsive at mobile (375px), tablet (768px), desktop (1920px)
|
||||||
|
- [ ] Interactive elements work (carousels, buttons, links)
|
||||||
|
- [ ] Keyboard navigation functional (Tab, Enter, Escape)
|
||||||
|
- [ ] Contrast ratios meet WCAG AA standards
|
||||||
|
- [ ] No console errors or warnings
|
||||||
|
- [ ] Network requests successful (no 404s)
|
||||||
|
- [ ] Animations smooth and respect reduced motion
|
||||||
|
- [ ] External links open correctly
|
||||||
|
- [ ] Images optimized and load properly
|
||||||
|
|
||||||
|
### Common Test Scenarios
|
||||||
|
|
||||||
|
#### Testing a New Section Component
|
||||||
|
```typescript
|
||||||
|
// 1. Check server is running
|
||||||
|
lsof -i :4000
|
||||||
|
|
||||||
|
// 2. Navigate and snapshot
|
||||||
|
mcp0_browser_navigate → http://localhost:4000
|
||||||
|
mcp0_browser_snapshot
|
||||||
|
|
||||||
|
// 3. Verify section appears
|
||||||
|
// Look for section in snapshot output
|
||||||
|
|
||||||
|
// 4. Test responsiveness
|
||||||
|
mcp0_browser_resize → 375x667 (mobile)
|
||||||
|
mcp0_browser_snapshot
|
||||||
|
mcp0_browser_resize → 1920x1080 (desktop)
|
||||||
|
mcp0_browser_snapshot
|
||||||
|
|
||||||
|
// 5. Check console
|
||||||
|
mcp0_browser_console_messages
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Testing Accessibility
|
||||||
|
```typescript
|
||||||
|
// 1. Check contrast
|
||||||
|
mcp0_browser_evaluate →
|
||||||
|
function: () => {
|
||||||
|
const el = document.querySelector('.your-element');
|
||||||
|
const styles = window.getComputedStyle(el);
|
||||||
|
return {
|
||||||
|
color: styles.color,
|
||||||
|
backgroundColor: styles.backgroundColor
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Test keyboard navigation
|
||||||
|
mcp0_browser_press_key → Tab
|
||||||
|
mcp0_browser_snapshot → verify focus
|
||||||
|
mcp0_browser_press_key → Enter
|
||||||
|
mcp0_browser_snapshot → verify action
|
||||||
|
|
||||||
|
// 3. Check ARIA
|
||||||
|
mcp0_browser_snapshot → review for proper labels
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Run Tests
|
||||||
|
|
||||||
|
- **Before committing**: Run basic visual and console checks
|
||||||
|
- **After adding new section**: Full section integration test
|
||||||
|
- **After styling changes**: Visual + contrast + responsive tests
|
||||||
|
- **After adding interactivity**: Functional + keyboard + accessibility tests
|
||||||
|
- **Before production build**: Complete checklist + all scenarios
|
||||||
|
|
||||||
|
### Debugging Failed Tests
|
||||||
|
|
||||||
|
1. **Visual issues**: Take screenshot with `mcp0_browser_take_screenshot` for detailed inspection
|
||||||
|
2. **Functional issues**: Use `mcp0_browser_evaluate` to inspect element state
|
||||||
|
3. **Network issues**: Check `mcp0_browser_network_requests` for failed requests
|
||||||
|
4. **Console errors**: Review `mcp0_browser_console_messages` for stack traces
|
||||||
|
5. **Timing issues**: Use `mcp0_browser_wait_for` to wait for elements/animations
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- This is a **portfolio website** showcasing projects, experience, achievements, and skills
|
||||||
|
- Runs on **port 4000** (not default 3000)
|
||||||
|
- Uses **pages router** (not app router)
|
||||||
|
- Strict TypeScript and ESLint configuration for code quality
|
||||||
|
- Tab-based indentation is non-negotiable (project standard)
|
||||||
|
- **Code complexity limits are strictly enforced** - refactor proactively
|
||||||
|
- **Always test changes with Playwright MCP** before committing
|
||||||
|
- **IMPORTANT: Always use Windsurf rules** - they are allowed by default and enable much faster AI development progress
|
||||||
46
Dockerfile
Normal file
46
Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
FROM node:18-alpine AS base
|
||||||
|
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
RUN corepack enable && corepack prepare pnpm@10.30.1 --activate
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM base AS deps
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
RUN pnpm install --frozen-lockfile --prod=false
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
FROM base AS runner
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
RUN mkdir .next
|
||||||
|
RUN chown nextjs:nodejs .next
|
||||||
|
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 4000
|
||||||
|
|
||||||
|
ENV PORT=4000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
95
README.md
95
README.md
@@ -1,23 +1,87 @@
|
|||||||
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 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).
|
||||||
|
|
||||||
|
## Package Manager
|
||||||
|
|
||||||
|
This project uses **PNPM** for dependency management, providing better disk space efficiency and faster installs through proper library linking.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
If you don't have PNPM installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g pnpm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
First, run the development server:
|
### Development Server
|
||||||
|
|
||||||
|
Run the development server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
Open [http://localhost:4000](http://localhost:4000) with your browser to see the result.
|
||||||
|
|
||||||
You can start editing the page by modifying `app/index.tsx`. The page auto-updates as you edit the file.
|
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||||
|
|
||||||
|
### Production Build
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
pnpm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop with `docker-compose down`.
|
||||||
|
|
||||||
|
## Working on Multiple Features (Git Worktrees)
|
||||||
|
|
||||||
|
Git worktrees allow you to work on multiple branches simultaneously without switching contexts or stashing changes.
|
||||||
|
|
||||||
|
### Create a new worktree
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git worktree add ../Portfolio_Remake-feature-name feature-branch-name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigate and install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ../Portfolio_Remake-feature-name
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### List all worktrees
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git worktree list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove a worktree when done
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git worktree remove ../Portfolio_Remake-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tip:** Run dev servers on different ports (e.g., main on `:4000`, feature on `:4001`) to work on both simultaneously.
|
||||||
|
|
||||||
## Learn More
|
## Learn More
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
To learn more about Next.js, take a look at the following resources:
|
||||||
@@ -27,8 +91,19 @@ To learn more about Next.js, take a look at the following resources:
|
|||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
## Deploy on Vercel
|
## Deployment
|
||||||
|
|
||||||
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.
|
Deploy to [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) or use Docker (see above).
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
## Available Scripts
|
||||||
|
|
||||||
|
| Script | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `pnpm dev` | Start development server on port 4000 |
|
||||||
|
| `pnpm build` | Build production application |
|
||||||
|
| `pnpm start` | Start production server |
|
||||||
|
| `pnpm lint` | Run ESLint checks |
|
||||||
|
| `pnpm lintfix` | Auto-fix ESLint issues |
|
||||||
|
| `pnpm security:audit` | Run security vulnerability scan |
|
||||||
|
| `pnpm security:check` | Generate security report |
|
||||||
|
| `pnpm security:outdated` | Check for outdated dependencies |
|
||||||
|
|||||||
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
services:
|
||||||
|
portfolio:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: portfolio:latest
|
||||||
|
container_name: portfolio-app
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "4000:4000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- NEXT_TELEMETRY_DISABLED=1
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:4000"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
- /app/.next/cache
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
cap_add:
|
||||||
|
- NET_BIND_SERVICE
|
||||||
@@ -3,6 +3,46 @@ const path = require("path");
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sassOptions: {
|
sassOptions: {
|
||||||
includePaths: [path.join(__dirname, "styles")],
|
includePaths: [path.join(__dirname, "styles")]
|
||||||
},
|
},
|
||||||
|
output: "standalone",
|
||||||
|
poweredByHeader: false,
|
||||||
|
compress: true,
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/:path*",
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "X-DNS-Prefetch-Control",
|
||||||
|
value: "on"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Strict-Transport-Security",
|
||||||
|
value: "max-age=63072000; includeSubDomains; preload"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "X-Frame-Options",
|
||||||
|
value: "SAMEORIGIN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "X-Content-Type-Options",
|
||||||
|
value: "nosniff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "X-XSS-Protection",
|
||||||
|
value: "1; mode=block"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Referrer-Policy",
|
||||||
|
value: "origin-when-cross-origin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Permissions-Policy",
|
||||||
|
value: "camera=(), microphone=(), geolocation=()"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
5126
package-lock.json
generated
5126
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
55
package.json
55
package.json
@@ -6,33 +6,46 @@
|
|||||||
"dev": "next dev -p 4000",
|
"dev": "next dev -p 4000",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start -p 4000",
|
"start": "next start -p 4000",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"lintfix": "next lint --fix",
|
||||||
|
"security:audit": "pnpm audit --audit-level=moderate",
|
||||||
|
"security:check": "pnpm audit --json > security-report.json || true",
|
||||||
|
"security:outdated": "pnpm outdated"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@splidejs/react-splide": "^0.7.12",
|
"@splidejs/react-splide": "^0.7.12",
|
||||||
"animejs": "^3.2.1",
|
"animejs": "^3.2.2",
|
||||||
"aos": "^2.3.4",
|
"aos": "^2.3.4",
|
||||||
"eslint": "8.42.0",
|
"eslint": "8.57.1",
|
||||||
"eslint-config-next": "13.4.5",
|
"eslint-config-next": "15.5.12",
|
||||||
"next": "^15.0.5",
|
"next": "^15.5.12",
|
||||||
"react": "^18.2.0",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-icons": "^4.9.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"sharp": "^0.32.4",
|
"sharp": "^0.34.5",
|
||||||
"typescript": "5.1.3"
|
"typescript": "5.9.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/eslint-plugin-next": "^13.4.12",
|
"@next/eslint-plugin-next": "^15.5.12",
|
||||||
"@types/aos": "^3.0.4",
|
"@types/aos": "^3.0.7",
|
||||||
"@types/node": "^20.4.8",
|
"@types/node": "^20.19.33",
|
||||||
"@types/react": "^18.2.18",
|
"@types/react": "^18.3.28",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
"@types/react-transition-group": "^4.4.10",
|
"@types/react-transition-group": "^4.4.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
||||||
"@typescript-eslint/parser": "^6.2.1",
|
"@typescript-eslint/parser": "^8.56.0",
|
||||||
"eslint-plugin-react": "^7.33.0",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"sass": "^1.64.1"
|
"sass": "^1.97.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"pnpm": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"minimatch": ">=10.2.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import "@splidejs/splide/css/sea-green";
|
|||||||
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
export default function MyApp({ Component, pageProps }: AppProps): JSX.Element {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// here you can add your aos options
|
// here you can add your aos options
|
||||||
AOS.init({
|
AOS.init({
|
||||||
offset: 100,
|
offset: 100
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Html, Head, Main, NextScript} from "next/document";
|
import { Html, Head, Main, NextScript } from "next/document";
|
||||||
|
|
||||||
export default function Document() {
|
export default function Document(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Html lang="en">
|
<Html lang="en">
|
||||||
<Head/>
|
<Head/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import MainPage from "@/src/portfolio/MainPage";
|
import MainPage from "@/src/portfolio/MainPage";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<MainPage/>
|
<MainPage/>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { Work_Sans } from "next/font/google";
|
|||||||
const workSans = Work_Sans({ subsets: ["latin"] });
|
const workSans = Work_Sans({ subsets: ["latin"] });
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={workSans.className}>{children}</body>
|
<body className={workSans.className}>{children}</body>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default function Home() {
|
export default function Home(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<h1>:/</h1>
|
<h1>:/</h1>
|
||||||
);
|
);
|
||||||
|
|||||||
3744
pnpm-lock.yaml
generated
Normal file
3744
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ import Achievements from "@/src/portfolio/sections/Achievements";
|
|||||||
import Footer from "@/src/portfolio/sections/Footer";
|
import Footer from "@/src/portfolio/sections/Footer";
|
||||||
import SkillsAndLinks from "@/src/portfolio/sections/SkillsAndLinks";
|
import SkillsAndLinks from "@/src/portfolio/sections/SkillsAndLinks";
|
||||||
|
|
||||||
const MainPage = () => {
|
const MainPage = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Intro/>
|
<Intro/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {AwardArgs} from "@/src/portfolio/helpers/Certificate";
|
import { AwardArgs } from "@/src/portfolio/helpers/Certificate";
|
||||||
|
|
||||||
const certificateData : AwardArgs[] = [
|
const certificateData : AwardArgs[] = [
|
||||||
{
|
{
|
||||||
@@ -16,20 +16,20 @@ const certificateData : AwardArgs[] = [
|
|||||||
city: "Warsaw",
|
city: "Warsaw",
|
||||||
awardDate: "July 2020",
|
awardDate: "July 2020",
|
||||||
notes: [
|
notes: [
|
||||||
"Overall: 7.5 / 9.0, equivalent to C1",
|
"Overall: 7.5 / 9.0, equivalent to C1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Project Management Fundamentals",
|
title: "Project Management Fundamentals",
|
||||||
institution: "Project Management Institute",
|
institution: "Project Management Institute",
|
||||||
city: "Warsaw",
|
city: "Warsaw",
|
||||||
awardDate: "April 2019",
|
awardDate: "April 2019"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Project Management Principles",
|
title: "Project Management Principles",
|
||||||
institution: "Project Management Institute",
|
institution: "Project Management Institute",
|
||||||
city: "Warsaw",
|
city: "Warsaw",
|
||||||
awardDate: "April 2019",
|
awardDate: "April 2019"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {EducationArgs} from "@/src/portfolio/helpers/Education";
|
import { EducationArgs } from "@/src/portfolio/helpers/Education";
|
||||||
|
|
||||||
const educationData : EducationArgs[] = [
|
const educationData : EducationArgs[] = [
|
||||||
{
|
{
|
||||||
@@ -39,7 +39,7 @@ const educationData : EducationArgs[] = [
|
|||||||
"Final exam in Physics: 97% percentile",
|
"Final exam in Physics: 97% percentile",
|
||||||
"Final exam in Mathematics: 92% percentile"
|
"Final exam in Mathematics: 92% percentile"
|
||||||
],
|
],
|
||||||
useWith: true, //todo fix this (i.e. this does nothing)
|
useWith: true //todo fix this (i.e. this does nothing)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {ProjectArguments, SkillEnum} from "@/src/portfolio/helpers/Project";
|
import { IProjectArguments, SkillEnum } from "@/src/portfolio/helpers/Project";
|
||||||
|
|
||||||
const projectData : ProjectArguments[] = [
|
const projectData : IProjectArguments[] = [
|
||||||
{
|
{
|
||||||
imagePath: "expertAgents.png",
|
imagePath: "expertAgents.png",
|
||||||
tech: [
|
tech: [
|
||||||
@@ -55,7 +55,7 @@ const projectData : ProjectArguments[] = [
|
|||||||
title: "Natural Computing: Implementing and analysis of PSO, GA and GP",
|
title: "Natural Computing: Implementing and analysis of PSO, GA and GP",
|
||||||
tech: [
|
tech: [
|
||||||
SkillEnum.python,
|
SkillEnum.python,
|
||||||
SkillEnum.numpy,
|
SkillEnum.numpy
|
||||||
],
|
],
|
||||||
text: "During my Master's program, I had the opportunity to take a course on Natural Computing, where I implemented and analyzed three major algorithms: Particle Swarm Optimization (PSO), Genetic Algorithms (GA), and Genetic Programming (GP). This coursework allowed me to gain hands-on experience with these powerful optimization techniques, which are inspired by natural phenomena such as swarm intelligence and evolution. Through this project, I developed a deep understanding of the underlying principles of natural computing and its potential applications in various fields, such as engineering, finance, and biology. I am excited to showcase my implementation and analysis of PSO, GA, and GP on my website and demonstrate my proficiency in natural computing techniques.",
|
text: "During my Master's program, I had the opportunity to take a course on Natural Computing, where I implemented and analyzed three major algorithms: Particle Swarm Optimization (PSO), Genetic Algorithms (GA), and Genetic Programming (GP). This coursework allowed me to gain hands-on experience with these powerful optimization techniques, which are inspired by natural phenomena such as swarm intelligence and evolution. Through this project, I developed a deep understanding of the underlying principles of natural computing and its potential applications in various fields, such as engineering, finance, and biology. I am excited to showcase my implementation and analysis of PSO, GA, and GP on my website and demonstrate my proficiency in natural computing techniques.",
|
||||||
github: "https://github.com/KuchtaVR6/nat_coursework",
|
github: "https://github.com/KuchtaVR6/nat_coursework",
|
||||||
@@ -70,7 +70,7 @@ const projectData : ProjectArguments[] = [
|
|||||||
SkillEnum.react,
|
SkillEnum.react,
|
||||||
SkillEnum.html,
|
SkillEnum.html,
|
||||||
SkillEnum.css,
|
SkillEnum.css,
|
||||||
SkillEnum.express,
|
SkillEnum.express
|
||||||
],
|
],
|
||||||
github: "https://github.com/KuchtaVR6/Learnopedia"
|
github: "https://github.com/KuchtaVR6/Learnopedia"
|
||||||
},
|
},
|
||||||
@@ -148,7 +148,13 @@ const projectData : ProjectArguments[] = [
|
|||||||
// {
|
// {
|
||||||
// imagePath: "port1.png",
|
// imagePath: "port1.png",
|
||||||
// title: "My previous portfolio website",
|
// title: "My previous portfolio website",
|
||||||
// text: "This website was created as a challenge to myself to create an eye-pleasing and portable website with limited time. I decided to make it purely using HTML and CSS, and for the portability, I have used Bootstrap CSS. The resulting product is an informative, simple and good looking portfolio, which I was quite happy with. Throughout this academic year, I have gained more confidence in using React, I have decided to remake my portfolio this time with a more interesting and responsive design in mind, whilst maintaining the readability of the older version.",
|
// text: "This website was created as a challenge to myself to create an eye-pleasing " +
|
||||||
|
// "and portable website with limited time. I decided to make it purely using HTML and CSS, " +
|
||||||
|
// "and for the portability, I have used Bootstrap CSS. The resulting product is an " +
|
||||||
|
// "informative, simple and good looking portfolio, which I was quite happy with. " +
|
||||||
|
// "Throughout this academic year, I have gained more confidence in using React, I have " +
|
||||||
|
// "decided to remake my portfolio this time with a more interesting and responsive design " +
|
||||||
|
// "in mind, whilst maintaining the readability of the older version.",
|
||||||
// tech: [
|
// tech: [
|
||||||
// SkillEnum.html,
|
// SkillEnum.html,
|
||||||
// SkillEnum.css,
|
// SkillEnum.css,
|
||||||
@@ -177,11 +183,17 @@ const projectData : ProjectArguments[] = [
|
|||||||
SkillEnum.linux,
|
SkillEnum.linux,
|
||||||
SkillEnum.design3d
|
SkillEnum.design3d
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
// {
|
// {
|
||||||
// imagePath: "port3.png",
|
// imagePath: "port3.png",
|
||||||
// title: "My current portfolio website",
|
// title: "My current portfolio website",
|
||||||
// text: "And finally, this website is my most recent project. Design-wise I wanted to keep the website minimalistic but stunning at the same time to show my skills, and I have kept accessibility in mind. I had created this project with plentiful react to experience and I created this website with a very high standard of code and with reusability in mind so that I don't have to rewrite this website in the future. Admittedly I will probably end up doing it anyway because I love coding and improving my websites. ",
|
// text: "And finally, this website is my most recent project. Design-wise I wanted to " +
|
||||||
|
// "keep the website minimalistic but stunning at the same time to show my skills, and I " +
|
||||||
|
// "have kept accessibility in mind. I had created this project with plentiful react to " +
|
||||||
|
// "experience and I created this website with a very high standard of code and with " +
|
||||||
|
// "reusability in mind so that I don't have to rewrite this website in the future. " +
|
||||||
|
// "Admittedly I will probably end up doing it anyway because I love coding and " +
|
||||||
|
// "improving my websites. ",
|
||||||
// tech: [
|
// tech: [
|
||||||
// SkillEnum.react,
|
// SkillEnum.react,
|
||||||
// SkillEnum.html,
|
// SkillEnum.html,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {SkillEnum} from "@/src/portfolio/helpers/Project";
|
import { SkillEnum } from "@/src/portfolio/helpers/Project";
|
||||||
import {ProficiencyLevel} from "@/src/portfolio/helpers/SkillDisplay";
|
import { ProficiencyLevel } from "@/src/portfolio/helpers/SkillDisplay";
|
||||||
|
|
||||||
export const skillsInCategories = {
|
export const skillsInCategories = {
|
||||||
"Programming Languages": [
|
"Programming Languages": [
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {WorkExperienceArgs} from "@/src/portfolio/helpers/WorkExperience";
|
import { WorkExperienceArgs } from "@/src/portfolio/helpers/WorkExperience";
|
||||||
|
|
||||||
const workExperienceData : WorkExperienceArgs[] = [
|
const workExperienceData : WorkExperienceArgs[] = [
|
||||||
{
|
{
|
||||||
@@ -97,7 +97,7 @@ const workExperienceData : WorkExperienceArgs[] = [
|
|||||||
country: "Poland",
|
country: "Poland",
|
||||||
startDate: "July 2018",
|
startDate: "July 2018",
|
||||||
endDate: "August 2018"
|
endDate: "August 2018"
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const workExperienceParagraph =
|
export const workExperienceParagraph =
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import React, {FC, useState} from "react";
|
import React, { FC, useState } from "react";
|
||||||
import { CSSTransition } from "react-transition-group";
|
import { CSSTransition } from "react-transition-group";
|
||||||
|
|
||||||
type args = {
|
type AccordionArgs = {
|
||||||
title : string,
|
title : string,
|
||||||
children : React.ReactNode,
|
children : React.ReactNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Accordion : FC<args> = ({ title, children }) => {
|
const Accordion : FC<AccordionArgs> = ({ title, children }): JSX.Element => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const toggleAccordion = () => {
|
const toggleAccordion = (): void => {
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import styles from "../styling/achievements.module.scss";
|
import styles from "../styling/achievements.module.scss";
|
||||||
|
|
||||||
export type AwardArgs = {
|
export type AwardArgs = {
|
||||||
@@ -11,7 +11,7 @@ export type AwardArgs = {
|
|||||||
|
|
||||||
const Award : FC<AwardArgs> = (props) => {
|
const Award : FC<AwardArgs> = (props) => {
|
||||||
|
|
||||||
const getCountryEmoji = () => {
|
const getCountryEmoji = (): string => {
|
||||||
switch (props.city) {
|
switch (props.city) {
|
||||||
case "Warsaw":
|
case "Warsaw":
|
||||||
return ", PL";
|
return ", PL";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import styles from "../styling/achievements.module.scss";
|
import styles from "../styling/achievements.module.scss";
|
||||||
|
|
||||||
export type EducationArgs = {
|
export type EducationArgs = {
|
||||||
@@ -14,7 +14,7 @@ export type EducationArgs = {
|
|||||||
|
|
||||||
const Education : FC<EducationArgs> = (props) => {
|
const Education : FC<EducationArgs> = (props) => {
|
||||||
|
|
||||||
const getCountryEmoji = () => {
|
const getCountryEmoji = (): string => {
|
||||||
switch (props.city) {
|
switch (props.city) {
|
||||||
case "Warsaw":
|
case "Warsaw":
|
||||||
return ", Poland";
|
return ", Poland";
|
||||||
@@ -26,7 +26,7 @@ const Education : FC<EducationArgs> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{fontSize: "1.1em"}} className={styles.education} data-aos={"fade-right"}>
|
<div style={{ fontSize: "1.1em" }} className={styles.education} data-aos={"fade-right"}>
|
||||||
<div>
|
<div>
|
||||||
<span className={styles.title}>{props.title}</span> in <b>{props.subtitle}</b>
|
<span className={styles.title}>{props.title}</span> in <b>{props.subtitle}</b>
|
||||||
<br/>
|
<br/>
|
||||||
@@ -48,10 +48,10 @@ const Education : FC<EducationArgs> = (props) => {
|
|||||||
{props.endDate ?
|
{props.endDate ?
|
||||||
<>
|
<>
|
||||||
<i>From: </i>
|
<i>From: </i>
|
||||||
<span style={{float:"right"}}>{props.startDate}</span>
|
<span style={{ float:"right" }}>{props.startDate}</span>
|
||||||
<br/>
|
<br/>
|
||||||
<i>To: </i>
|
<i>To: </i>
|
||||||
<span style={{float:"right"}}>{props.endDate}</span>
|
<span style={{ float:"right" }}>{props.endDate}</span>
|
||||||
</>:
|
</>:
|
||||||
"Since " + props.startDate
|
"Since " + props.startDate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
import {FC, MutableRefObject, useEffect, useRef, useState} from "react";
|
import { FC, MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
type ProgressBarArgs = {
|
||||||
type args = {
|
|
||||||
percentage : number,
|
percentage : number,
|
||||||
customScore? : string,
|
customScore? : string,
|
||||||
}
|
}
|
||||||
const ProgressBar : FC<args> = ({percentage,customScore}) => {
|
const ProgressBar : FC<ProgressBarArgs> = ({ percentage,customScore }): JSX.Element => {
|
||||||
const ref = useRef<HTMLDivElement>() as MutableRefObject<HTMLDivElement>;
|
const ref = useRef<HTMLDivElement>() as MutableRefObject<HTMLDivElement>;
|
||||||
const onScreen = useOnScreen(ref);
|
const onScreen = useOnScreen(ref);
|
||||||
|
|
||||||
const [current,setCurrent] = useState(0);
|
const [current,setCurrent] = useState(0);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(() => {
|
||||||
if(onScreen && current==0)
|
if (onScreen && current==0)
|
||||||
{
|
{
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
if(counter<percentage*2)
|
if (counter<percentage*2)
|
||||||
{
|
{
|
||||||
counter += (percentage/100);
|
counter += (percentage/100);
|
||||||
setCurrent(counter);
|
setCurrent(counter);
|
||||||
}
|
}
|
||||||
else{
|
else {
|
||||||
if(counter>percentage*2)
|
if (counter>percentage*2)
|
||||||
{
|
{
|
||||||
setCurrent(percentage*2);
|
setCurrent(percentage*2);
|
||||||
}
|
}
|
||||||
@@ -30,20 +29,20 @@ const ProgressBar : FC<args> = ({percentage,customScore}) => {
|
|||||||
}
|
}
|
||||||
},25);
|
},25);
|
||||||
}
|
}
|
||||||
},[onScreen]);
|
},[onScreen, current, percentage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"pBContainer"} ref={ref}>
|
<div className={"pBContainer"} ref={ref}>
|
||||||
<div className={"progressBar"}>
|
<div className={"progressBar"}>
|
||||||
<div className={"inner"} style={{width : `${current/2}%`}}></div>
|
<div className={"inner"} style={{ width : `${current/2}%` }}></div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"below"} style={{width : customScore? "100%" : `${current/2}%`, textAlign: "right"}}>{customScore? customScore : Math.ceil(current/2)+"%"}</div>
|
<div className={"below"} style={{ width : customScore? "100%" : `${current/2}%`, textAlign: "right" }}>{customScore? customScore : Math.ceil(current/2)+"%"}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function useOnScreen(ref : MutableRefObject<HTMLDivElement>, rootMargin = "0px") {
|
function useOnScreen(ref : MutableRefObject<HTMLDivElement>, rootMargin = "0px"): boolean {
|
||||||
// State and setter for storing whether element is visible
|
// State and setter for storing whether element is visible
|
||||||
const [isIntersecting, setIntersecting] = useState(false);
|
const [isIntersecting, setIntersecting] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -53,21 +52,24 @@ function useOnScreen(ref : MutableRefObject<HTMLDivElement>, rootMargin = "0px")
|
|||||||
setIntersecting(entry.isIntersecting);
|
setIntersecting(entry.isIntersecting);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rootMargin,
|
rootMargin
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (ref.current) {
|
const currentRef = ref.current;
|
||||||
observer.observe(ref.current);
|
if (currentRef) {
|
||||||
|
observer.observe(currentRef);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
try{
|
if (currentRef) {
|
||||||
observer.unobserve(ref.current);
|
try {
|
||||||
}
|
observer.unobserve(currentRef);
|
||||||
catch (e) {
|
}
|
||||||
console.log("REPORTED: observer failure :/");
|
catch {
|
||||||
|
// Observer failure handled silently
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []); // Empty array ensures that effect is only run on mount and unmount
|
}, [ref, rootMargin]);
|
||||||
return isIntersecting;
|
return isIntersecting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import styles from "../styling/projects.module.scss";
|
import styles from "../styling/projects.module.scss";
|
||||||
import {VscGithub} from "react-icons/vsc";
|
import { VscGithub } from "react-icons/vsc";
|
||||||
import {BsGlobe2, BsFileEarmarkMedical} from "react-icons/bs";
|
import { BsGlobe2, BsFileEarmarkMedical } from "react-icons/bs";
|
||||||
|
|
||||||
export interface ProjectArguments {
|
export interface IProjectArguments {
|
||||||
imagePath : string,
|
imagePath : string,
|
||||||
title : string,
|
title : string,
|
||||||
text : string,
|
text : string,
|
||||||
@@ -47,7 +47,7 @@ export enum SkillEnum {
|
|||||||
english = "English",
|
english = "English",
|
||||||
}
|
}
|
||||||
|
|
||||||
const Project = ({imagePath, title, text, github, access, document} : ProjectArguments) => {
|
const Project = ({ imagePath, title, text, github, access, document } : IProjectArguments): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.projectDisplay}>
|
<div className={styles.projectDisplay}>
|
||||||
<div className={styles.text}>
|
<div className={styles.text}>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import styles from "../styling/projects.module.scss";
|
import styles from "../styling/projects.module.scss";
|
||||||
import {SkillEnum} from "@/src/portfolio/helpers/Project";
|
import { SkillEnum } from "@/src/portfolio/helpers/Project";
|
||||||
|
|
||||||
type args = {
|
type SkillDisplayArgs = {
|
||||||
name : SkillEnum,
|
name : SkillEnum,
|
||||||
level? : ProficiencyLevel,
|
level? : ProficiencyLevel,
|
||||||
}
|
}
|
||||||
@@ -15,8 +15,7 @@ export enum ProficiencyLevel {
|
|||||||
master = "Master",
|
master = "Master",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SkillDisplay : FC<SkillDisplayArgs> = ({ name, level }): JSX.Element => {
|
||||||
const SkillDisplay : FC<args> = ({name, level}) => {
|
|
||||||
const level_to_emoji = {
|
const level_to_emoji = {
|
||||||
[ProficiencyLevel.beginner]: "🌱",
|
[ProficiencyLevel.beginner]: "🌱",
|
||||||
[ProficiencyLevel.intermediate]: "🌳",
|
[ProficiencyLevel.intermediate]: "🌳",
|
||||||
@@ -29,7 +28,7 @@ const SkillDisplay : FC<args> = ({name, level}) => {
|
|||||||
return (
|
return (
|
||||||
<div id={name.toLowerCase()} className={styles.technology}>
|
<div id={name.toLowerCase()} className={styles.technology}>
|
||||||
<b>{name} </b>
|
<b>{name} </b>
|
||||||
<span style={{float: "right", fontSize: "0.8em"}}>{level} {level_to_emoji[level]}</span>
|
<span style={{ float: "right", fontSize: "0.8em" }}>{level} {level_to_emoji[level]}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import styles from "../styling/experience.module.scss";
|
import styles from "../styling/experience.module.scss";
|
||||||
|
|
||||||
export type WorkExperienceArgs = {
|
export type WorkExperienceArgs = {
|
||||||
@@ -13,7 +13,7 @@ export type WorkExperienceArgs = {
|
|||||||
|
|
||||||
const WorkExperience : FC<WorkExperienceArgs> = (props) => {
|
const WorkExperience : FC<WorkExperienceArgs> = (props) => {
|
||||||
|
|
||||||
const getEmojiForIndustry = () => {
|
const getEmojiForIndustry = (): string | undefined => {
|
||||||
switch (props.industry) {
|
switch (props.industry) {
|
||||||
case "Education":
|
case "Education":
|
||||||
return "🎓";
|
return "🎓";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import styles from "../styling/achievements.module.scss";
|
|||||||
import educationData from "@/src/portfolio/data/educationData";
|
import educationData from "@/src/portfolio/data/educationData";
|
||||||
import certificateData from "@/src/portfolio/data/certificateData";
|
import certificateData from "@/src/portfolio/data/certificateData";
|
||||||
|
|
||||||
const Achievements = () => {
|
const Achievements = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.section} id={"achievements"}>
|
<div className={styles.section} id={"achievements"}>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {Splide, SplideSlide} from "@splidejs/react-splide";
|
import { Splide, SplideSlide } from "@splidejs/react-splide";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import styles from "../styling/experience.module.scss";
|
import styles from "../styling/experience.module.scss";
|
||||||
import WorkExperience from "@/src/portfolio/helpers/WorkExperience";
|
import WorkExperience from "@/src/portfolio/helpers/WorkExperience";
|
||||||
import workExperienceData, {workExperienceParagraph} from "@/src/portfolio/data/workExperienceData";
|
import workExperienceData, { workExperienceParagraph } from "@/src/portfolio/data/workExperienceData";
|
||||||
|
|
||||||
const Experience = () => {
|
const Experience = (): JSX.Element => {
|
||||||
const calcPagesOnWidth = (width : number) => {
|
const calcPagesOnWidth = (width : number): number => {
|
||||||
return Math.floor(width / 800 + 1);
|
return Math.floor(width / 800 + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ const Experience = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
setPages(calcPagesOnWidth(window.innerWidth));
|
setPages(calcPagesOnWidth(window.innerWidth));
|
||||||
const handleResize = () => {
|
const handleResize = (): void => {
|
||||||
setPages(calcPagesOnWidth(window.innerWidth));
|
setPages(calcPagesOnWidth(window.innerWidth));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import styles from "../styling/footer.module.scss";
|
import styles from "../styling/footer.module.scss";
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<footer className={styles.section}>
|
<footer className={styles.section}>
|
||||||
Copyright © Patryk Kuchta {new Date().getFullYear()}
|
Copyright © Patryk Kuchta {new Date().getFullYear()}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import styles from "../styling/intro.module.scss";
|
|
||||||
import Image from "next/image";
|
|
||||||
import profilePic from "../../../public/portfolio/profile.png";
|
|
||||||
import logo from "../../../public/portfolio/logo.svg";
|
|
||||||
|
|
||||||
const Intro = () => {
|
|
||||||
|
|
||||||
const greeting = () => {
|
|
||||||
const hour = new Date().getHours();
|
|
||||||
if (hour > 4 && hour < 12) {
|
|
||||||
return "Good Morning!";
|
|
||||||
} else if (hour < 18) {
|
|
||||||
return "Good Afternoon!";
|
|
||||||
} else {
|
|
||||||
return "Good Evening!";
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className={styles.section}>
|
|
||||||
<header className={styles.topBar} data-aos={"zoom-in-down"} data-aos-duration={"500"}>
|
|
||||||
<div className={styles.row}>
|
|
||||||
<div className={styles.logoContainer}>
|
|
||||||
<Image src={logo} alt={"Kuchta logo"} fill={true}/>
|
|
||||||
</div>
|
|
||||||
<a href={"#achievements"}>Achievements</a>
|
|
||||||
<a href={"#experience"}>Experience</a>
|
|
||||||
<a href={"#projects"}>Projects</a>
|
|
||||||
<a href={"#skills"}>Skills</a>
|
|
||||||
</div>
|
|
||||||
<div className={styles.left}>
|
|
||||||
<a href={"mailto: patrick@kuchta.uk"}>patrick@kuchta.uk</a>
|
|
||||||
<span>/</span>
|
|
||||||
<a href={"mailto: patryk@kuchta.uk"}>patryk@kuchta.uk</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div className={styles.mainContent}>
|
|
||||||
<div className={styles.text} data-aos={"fade-in"} data-aos-duration={"500"}>
|
|
||||||
<div className={styles.larger}>
|
|
||||||
<span className={styles.greeting}>
|
|
||||||
{greeting()}
|
|
||||||
</span>
|
|
||||||
<br/>
|
|
||||||
I am
|
|
||||||
<b> Patryk Kuchta, </b>
|
|
||||||
an aspiring
|
|
||||||
<br/>
|
|
||||||
<b>Artificial Intelligence Scientist</b>.
|
|
||||||
</div>
|
|
||||||
<span data-aos={"fade-in"} data-aos-offset={"200"}><i>and yes, the greeting is synced to your time.</i></span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.profileContainer}>
|
|
||||||
<Image
|
|
||||||
src={profilePic}
|
|
||||||
alt={"Frontal image showing Patryk Kuchta"}
|
|
||||||
fill={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Intro;
|
|
||||||
17
src/portfolio/sections/Intro/Intro.tsx
Normal file
17
src/portfolio/sections/Intro/Intro.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import styles from "../../styling/intro.module.scss";
|
||||||
|
import TopBar from "./TopBar";
|
||||||
|
import IntroContent from "./IntroContent";
|
||||||
|
import { getGreeting } from "./helpers";
|
||||||
|
|
||||||
|
const Intro = (): JSX.Element => {
|
||||||
|
const greeting = getGreeting();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<TopBar/>
|
||||||
|
<IntroContent greeting={greeting}/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Intro;
|
||||||
39
src/portfolio/sections/Intro/IntroContent.tsx
Normal file
39
src/portfolio/sections/Intro/IntroContent.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import styles from "../../styling/intro.module.scss";
|
||||||
|
import profilePic from "../../../../public/portfolio/profile.png";
|
||||||
|
|
||||||
|
interface IIntroContentProps {
|
||||||
|
greeting: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IntroContent = ({ greeting }: IIntroContentProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className={styles.mainContent}>
|
||||||
|
<div className={styles.text} data-aos={"fade-in"} data-aos-duration={"500"}>
|
||||||
|
<div className={styles.larger}>
|
||||||
|
<span className={styles.greeting}>
|
||||||
|
{greeting}
|
||||||
|
</span>
|
||||||
|
<br/>
|
||||||
|
I am
|
||||||
|
<b> Patryk Kuchta, </b>
|
||||||
|
an aspiring
|
||||||
|
<br/>
|
||||||
|
<b>Artificial Intelligence Scientist</b>.
|
||||||
|
</div>
|
||||||
|
<span data-aos={"fade-in"} data-aos-offset={"200"}>
|
||||||
|
<i>and yes, the greeting is synced to your time.</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.profileContainer}>
|
||||||
|
<Image
|
||||||
|
src={profilePic}
|
||||||
|
alt={"Frontal image showing Patryk Kuchta"}
|
||||||
|
fill={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IntroContent;
|
||||||
26
src/portfolio/sections/Intro/TopBar.tsx
Normal file
26
src/portfolio/sections/Intro/TopBar.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import styles from "../../styling/intro.module.scss";
|
||||||
|
import logo from "../../../../public/portfolio/logo.svg";
|
||||||
|
|
||||||
|
const TopBar = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<header className={styles.topBar} data-aos={"zoom-in-down"} data-aos-duration={"500"}>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<div className={styles.logoContainer}>
|
||||||
|
<Image src={logo} alt={"Kuchta logo"} fill={true}/>
|
||||||
|
</div>
|
||||||
|
<a href={"#achievements"}>Achievements</a>
|
||||||
|
<a href={"#experience"}>Experience</a>
|
||||||
|
<a href={"#projects"}>Projects</a>
|
||||||
|
<a href={"#skills"}>Skills</a>
|
||||||
|
</div>
|
||||||
|
<div className={styles.left}>
|
||||||
|
<a href={"mailto: patrick@kuchta.uk"}>patrick@kuchta.uk</a>
|
||||||
|
<span>/</span>
|
||||||
|
<a href={"mailto: patryk@kuchta.uk"}>patryk@kuchta.uk</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TopBar;
|
||||||
10
src/portfolio/sections/Intro/helpers.ts
Normal file
10
src/portfolio/sections/Intro/helpers.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export const getGreeting = (): string => {
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
if (hour > 4 && hour < 12) {
|
||||||
|
return "Good Morning!";
|
||||||
|
} else if (hour < 18) {
|
||||||
|
return "Good Afternoon!";
|
||||||
|
} else {
|
||||||
|
return "Good Evening!";
|
||||||
|
}
|
||||||
|
};
|
||||||
1
src/portfolio/sections/Intro/index.ts
Normal file
1
src/portfolio/sections/Intro/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./Intro";
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import {Splide, SplideSlide} from "@splidejs/react-splide";
|
import { Splide, SplideSlide } from "@splidejs/react-splide";
|
||||||
import Project from "@/src/portfolio/helpers/Project";
|
import Project from "@/src/portfolio/helpers/Project";
|
||||||
import projectData from "@/src/portfolio/data/projectData";
|
import projectData from "@/src/portfolio/data/projectData";
|
||||||
import styles from "../styling/projects.module.scss";
|
import styles from "../styling/projects.module.scss";
|
||||||
|
|
||||||
const Projects = () => {
|
const Projects = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.section} id={"projects"}>
|
<div className={styles.section} id={"projects"}>
|
||||||
<Splide
|
<Splide
|
||||||
options={{
|
options={{
|
||||||
rewind: true,
|
rewind: true,
|
||||||
type: "slide",
|
type: "slide",
|
||||||
perPage: 1,
|
perPage: 1
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ projectData.map((entry, key) => {
|
{ projectData.map((entry, key) => {
|
||||||
|
|||||||
@@ -1,107 +1 @@
|
|||||||
import styles from "../styling/skillsLinks.module.scss";
|
export { default } from "./SkillsAndLinks/SkillsAndLinks";
|
||||||
import SkillDisplay from "@/src/portfolio/helpers/SkillDisplay";
|
|
||||||
import Accordion from "@/src/portfolio/helpers/Accordion";
|
|
||||||
import {skillsInCategories} from "@/src/portfolio/data/skillsData";
|
|
||||||
import {modulesTaken} from "@/src/portfolio/data/modulesTaken";
|
|
||||||
|
|
||||||
const SkillsAndLinks = () => {
|
|
||||||
const moreInfoLinks = [
|
|
||||||
{
|
|
||||||
title: "Github",
|
|
||||||
link: "https://github.com/KuchtaVR6/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "LinkedIn",
|
|
||||||
link: "https://linkedin.com/in/kuchtap"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Curriculum Vitae",
|
|
||||||
link: "/PatrykKuchta_CV.pdf"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const contactLinks = [
|
|
||||||
{
|
|
||||||
title: "Email",
|
|
||||||
link: "mailto:patryk@kuchta.uk"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "LinkedIn",
|
|
||||||
link: "https://linkedin.com/in/kuchtap"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.section} id={"skills"}>
|
|
||||||
<div data-aos = {"fade-left"} className={styles.skills}>
|
|
||||||
<div>
|
|
||||||
<i>Press the "+" to expand a section</i> 👀
|
|
||||||
</div>
|
|
||||||
<div className={styles.innerskills}>
|
|
||||||
{
|
|
||||||
Object.entries(skillsInCategories).map(([category, skills]) => {
|
|
||||||
console.log(skills);
|
|
||||||
return <Accordion title={category} key={category}>
|
|
||||||
{
|
|
||||||
skills.map((skill) => {
|
|
||||||
return <SkillDisplay {...skill} key={skill.name} />;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Accordion>;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<Accordion title={"Modules Taken"}>
|
|
||||||
{
|
|
||||||
modulesTaken.map(({name, level, score}) => {
|
|
||||||
return <div id={name.toLowerCase()}
|
|
||||||
className={styles.technology}
|
|
||||||
key={name}>
|
|
||||||
<b>{name} </b>
|
|
||||||
<span style={{float: "right", fontSize: "0.8em"}}>
|
|
||||||
{score>=0? score + "%" : "🔮"} <i>{level}</i>
|
|
||||||
</span>
|
|
||||||
</div>;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div data-aos = {"fade-right"} className={styles.otherLinks}>
|
|
||||||
<h2>Find out more about me:</h2>
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
moreInfoLinks.map(({title, link}, key) => {
|
|
||||||
return (
|
|
||||||
<li key = {key}>
|
|
||||||
<a href={link}>
|
|
||||||
{title}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
<h2>Contact me through:</h2>
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
contactLinks.map(({title, link}, key) => {
|
|
||||||
return (
|
|
||||||
<li key = {key}>
|
|
||||||
<a href={link}>
|
|
||||||
{title}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SkillsAndLinks;
|
|
||||||
|
|||||||
71
src/portfolio/sections/SkillsAndLinks/LinksSection.tsx
Normal file
71
src/portfolio/sections/SkillsAndLinks/LinksSection.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import styles from "../../styling/skillsLinks.module.scss";
|
||||||
|
|
||||||
|
interface ILink {
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moreInfoLinks: ILink[] = [
|
||||||
|
{
|
||||||
|
title: "Github",
|
||||||
|
link: "https://github.com/KuchtaVR6/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "LinkedIn",
|
||||||
|
link: "https://linkedin.com/in/kuchtap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Curriculum Vitae",
|
||||||
|
link: "/PatrykKuchta_CV.pdf"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const contactLinks: ILink[] = [
|
||||||
|
{
|
||||||
|
title: "Email",
|
||||||
|
link: "mailto:patryk@kuchta.uk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "LinkedIn",
|
||||||
|
link: "https://linkedin.com/in/kuchtap"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const LinksSection = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div data-aos = {"fade-right"} className={styles.otherLinks}>
|
||||||
|
<h2>Find out more about me:</h2>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
moreInfoLinks.map(({ title, link }, key) => {
|
||||||
|
return (
|
||||||
|
<li key = {key}>
|
||||||
|
<a href={link}>
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<h2>Contact me through:</h2>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
contactLinks.map(({ title, link }, key) => {
|
||||||
|
return (
|
||||||
|
<li key = {key}>
|
||||||
|
<a href={link}>
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LinksSection;
|
||||||
14
src/portfolio/sections/SkillsAndLinks/SkillsAndLinks.tsx
Normal file
14
src/portfolio/sections/SkillsAndLinks/SkillsAndLinks.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import styles from "../../styling/skillsLinks.module.scss";
|
||||||
|
import SkillsSection from "./SkillsSection";
|
||||||
|
import LinksSection from "./LinksSection";
|
||||||
|
|
||||||
|
const SkillsAndLinks = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className={styles.section} id={"skills"}>
|
||||||
|
<SkillsSection/>
|
||||||
|
<LinksSection/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SkillsAndLinks;
|
||||||
45
src/portfolio/sections/SkillsAndLinks/SkillsSection.tsx
Normal file
45
src/portfolio/sections/SkillsAndLinks/SkillsSection.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import styles from "../../styling/skillsLinks.module.scss";
|
||||||
|
import SkillDisplay from "@/src/portfolio/helpers/SkillDisplay";
|
||||||
|
import Accordion from "@/src/portfolio/helpers/Accordion";
|
||||||
|
import { skillsInCategories } from "@/src/portfolio/data/skillsData";
|
||||||
|
import { modulesTaken } from "@/src/portfolio/data/modulesTaken";
|
||||||
|
|
||||||
|
const SkillsSection = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div data-aos = {"fade-left"} className={styles.skills}>
|
||||||
|
<div>
|
||||||
|
<i>Press the "+" to expand a section</i> 👀
|
||||||
|
</div>
|
||||||
|
<div className={styles.innerskills}>
|
||||||
|
{
|
||||||
|
Object.entries(skillsInCategories).map(([category, skills]) => {
|
||||||
|
return <Accordion title={category} key={category}>
|
||||||
|
{
|
||||||
|
skills.map((skill) => {
|
||||||
|
return <SkillDisplay {...skill} key={skill.name} />;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Accordion>;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<Accordion title={"Modules Taken"}>
|
||||||
|
{
|
||||||
|
modulesTaken.map(({ name, level, score }) => {
|
||||||
|
return <div id={name.toLowerCase()}
|
||||||
|
className={styles.technology}
|
||||||
|
key={name}>
|
||||||
|
<b>{name} </b>
|
||||||
|
<span style={{ float: "right", fontSize: "0.8em" }}>
|
||||||
|
{score>=0? score + "%" : "🔮"} <i>{level}</i>
|
||||||
|
</span>
|
||||||
|
</div>;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SkillsSection;
|
||||||
1
src/portfolio/sections/SkillsAndLinks/index.ts
Normal file
1
src/portfolio/sections/SkillsAndLinks/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./SkillsAndLinks";
|
||||||
47
windsurf-commands.sh
Executable file
47
windsurf-commands.sh
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Windsurf Commands Script
|
||||||
|
# This script provides common development commands for the portfolio project
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
echo "Usage: ./windsurf-commands.sh [COMMAND]"
|
||||||
|
echo ""
|
||||||
|
echo "Available commands:"
|
||||||
|
echo " lint - Run ESLint to check for code issues"
|
||||||
|
echo " lintfix - Run ESLint with auto-fix enabled"
|
||||||
|
echo " help - Show this help message"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
run_lint() {
|
||||||
|
echo "🔍 Running ESLint..."
|
||||||
|
npm run lint
|
||||||
|
echo "✅ Linting complete!"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_lintfix() {
|
||||||
|
echo "🔧 Running ESLint with auto-fix..."
|
||||||
|
npm run lintfix
|
||||||
|
echo "✅ Linting with auto-fix complete!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main script logic
|
||||||
|
case "${1:-help}" in
|
||||||
|
lint)
|
||||||
|
run_lint
|
||||||
|
;;
|
||||||
|
lintfix)
|
||||||
|
run_lintfix
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ Unknown command: $1"
|
||||||
|
echo ""
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user