Merge pull request #2 from KuchtaVR6/modern-project-configuration
Modern Project Configuration
This commit is contained in:
@@ -28,6 +28,87 @@
|
||||
"error",
|
||||
"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 }]
|
||||
}
|
||||
}
|
||||
|
||||
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: 10
|
||||
|
||||
- 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."
|
||||
71
.github/workflows/typecheck.yml
vendored
Normal file
71
.github/workflows/typecheck.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Code Quality Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
quality-check:
|
||||
name: Lint & Type Check
|
||||
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: 10
|
||||
|
||||
- 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 ESLint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Run TypeScript type check
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Comment PR on failure
|
||||
if: failure() && github.event_name == 'pull_request'
|
||||
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: '❌ Code quality check failed. Please run `./windsurf-commands.sh lintfix` and `./windsurf-commands.sh typecheck` locally to fix issues.'
|
||||
})
|
||||
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "::error::Code quality check failed. Please fix linting and type errors before merging."
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -4,6 +4,8 @@
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.pnpm-store
|
||||
pnpm-debug.log*
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
@@ -33,3 +35,10 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
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
|
||||
421
AGENTS.md
Normal file
421
AGENTS.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# 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
|
||||
|
||||
### 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 typecheck # Run TypeScript type checking
|
||||
./windsurf-commands.sh security:audit # Run security vulnerability scan
|
||||
./windsurf-commands.sh security:check # Generate security report (JSON)
|
||||
./windsurf-commands.sh security:outdated # Check for outdated dependencies
|
||||
./windsurf-commands.sh help # Show available commands
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
- Script provides consistent interface for development commands
|
||||
- All commands use `pnpm` internally
|
||||
- Includes error handling and user-friendly output with emojis
|
||||
- Avoid using `pnpm run` commands directly
|
||||
|
||||
## 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
|
||||
|
||||
## CI/CD Pipelines
|
||||
|
||||
The project includes automated GitHub Actions workflows for code quality and security:
|
||||
|
||||
### Lint Check Workflows
|
||||
- **Push Lint Check** (`.github/workflows/push-lint-check.yml`)
|
||||
- Runs on every push to any branch
|
||||
- Executes ESLint checks
|
||||
- Uses pnpm v10 with caching for faster builds
|
||||
- Fails if linting errors are found
|
||||
|
||||
- **PR Lint Check** (`.github/workflows/pr-lint-check.yml`)
|
||||
- Runs on pull requests to main/master/develop
|
||||
- Executes ESLint checks
|
||||
- Posts comment on PR if linting fails
|
||||
- Uses pnpm v10 with caching
|
||||
|
||||
### TypeScript Type Check Workflow
|
||||
- **TypeScript Type Check** (`.github/workflows/typecheck.yml`)
|
||||
- Runs on every push and pull request
|
||||
- Executes `tsc --noEmit` to check for type errors
|
||||
- Uses pnpm v10 with caching
|
||||
- Fails if type errors are found
|
||||
|
||||
### Security Audit Workflow
|
||||
- **Security Audit** (`.github/workflows/security-audit.yml`)
|
||||
- Runs on push to main/master/develop
|
||||
- Runs on all pull requests
|
||||
- Runs weekly on Monday at 00:00 UTC (scheduled)
|
||||
- Executes `pnpm audit` for vulnerability scanning
|
||||
- Generates security report (JSON) as artifact
|
||||
- Checks for outdated dependencies
|
||||
- Uses pnpm v10 with caching
|
||||
- Continues on error but uploads report for review
|
||||
|
||||
### Pipeline Requirements
|
||||
All workflows require:
|
||||
- Node.js 18
|
||||
- pnpm v10 (matches lockfile version)
|
||||
- Frozen lockfile (`pnpm install --frozen-lockfile`)
|
||||
- Proper caching of pnpm store for performance
|
||||
|
||||
### Docker Deployment
|
||||
- **Dockerfile**: Multi-stage build with Alpine Linux base
|
||||
- Uses pnpm v10.30.1
|
||||
- Non-root user (UID 1001)
|
||||
- Read-only filesystem with security hardening
|
||||
- Standalone Next.js output (~150MB image)
|
||||
|
||||
- **docker-compose.yml**: One-command deployment
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
- Includes health checks
|
||||
- Security options (no-new-privileges, dropped capabilities)
|
||||
- Runs on port 4000
|
||||
|
||||
## 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"]
|
||||
93
README.md
93
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).
|
||||
|
||||
## 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
|
||||
|
||||
First, run the development server:
|
||||
### Development Server
|
||||
|
||||
Run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
@@ -27,8 +91,15 @@ 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!
|
||||
|
||||
## Deploy on Vercel
|
||||
## Available Scripts
|
||||
|
||||
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.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
| 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", "-q", "-O", "/dev/null", "--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 = {
|
||||
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
56
package.json
56
package.json
@@ -6,33 +6,47 @@
|
||||
"dev": "next dev -p 4000",
|
||||
"build": "next build",
|
||||
"start": "next start -p 4000",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"lintfix": "next lint --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"security:audit": "pnpm audit --audit-level=moderate",
|
||||
"security:check": "pnpm audit --json > security-report.json || true",
|
||||
"security:outdated": "pnpm outdated"
|
||||
},
|
||||
"dependencies": {
|
||||
"@splidejs/react-splide": "^0.7.12",
|
||||
"animejs": "^3.2.1",
|
||||
"animejs": "^3.2.2",
|
||||
"aos": "^2.3.4",
|
||||
"eslint": "8.42.0",
|
||||
"eslint-config-next": "13.4.5",
|
||||
"next": "^15.0.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.9.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-next": "15.5.12",
|
||||
"next": "^15.5.12",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"sharp": "^0.32.4",
|
||||
"typescript": "5.1.3"
|
||||
"sharp": "^0.34.5",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^13.4.12",
|
||||
"@types/aos": "^3.0.4",
|
||||
"@types/node": "^20.4.8",
|
||||
"@types/react": "^18.2.18",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-transition-group": "^4.4.10",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
"@typescript-eslint/parser": "^6.2.1",
|
||||
"eslint-plugin-react": "^7.33.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"sass": "^1.64.1"
|
||||
"@next/eslint-plugin-next": "^15.5.12",
|
||||
"@types/aos": "^3.0.7",
|
||||
"@types/node": "^20.19.33",
|
||||
"@types/react": "^18.3.28",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
||||
"@typescript-eslint/parser": "^8.56.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"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";
|
||||
|
||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
export default function MyApp({ Component, pageProps }: AppProps): JSX.Element {
|
||||
useEffect(() => {
|
||||
// here you can add your aos options
|
||||
AOS.init({
|
||||
offset: 100,
|
||||
offset: 100
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Html, Head, Main, NextScript } from "next/document";
|
||||
|
||||
export default function Document() {
|
||||
export default function Document(): JSX.Element {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import MainPage from "@/src/portfolio/MainPage";
|
||||
|
||||
export default function Home() {
|
||||
export default function Home(): JSX.Element {
|
||||
return (
|
||||
<main>
|
||||
<MainPage/>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Work_Sans } from "next/font/google";
|
||||
|
||||
const workSans = Work_Sans({ subsets: ["latin"] });
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={workSans.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<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 SkillsAndLinks from "@/src/portfolio/sections/SkillsAndLinks";
|
||||
|
||||
const MainPage = () => {
|
||||
const MainPage = (): JSX.Element => {
|
||||
return (
|
||||
<div>
|
||||
<Intro/>
|
||||
|
||||
@@ -16,20 +16,20 @@ const certificateData : AwardArgs[] = [
|
||||
city: "Warsaw",
|
||||
awardDate: "July 2020",
|
||||
notes: [
|
||||
"Overall: 7.5 / 9.0, equivalent to C1",
|
||||
"Overall: 7.5 / 9.0, equivalent to C1"
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Project Management Fundamentals",
|
||||
institution: "Project Management Institute",
|
||||
city: "Warsaw",
|
||||
awardDate: "April 2019",
|
||||
awardDate: "April 2019"
|
||||
},
|
||||
{
|
||||
title: "Project Management Principles",
|
||||
institution: "Project Management Institute",
|
||||
city: "Warsaw",
|
||||
awardDate: "April 2019",
|
||||
awardDate: "April 2019"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ const educationData : EducationArgs[] = [
|
||||
"Final exam in Physics: 97% 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",
|
||||
tech: [
|
||||
@@ -55,7 +55,7 @@ const projectData : ProjectArguments[] = [
|
||||
title: "Natural Computing: Implementing and analysis of PSO, GA and GP",
|
||||
tech: [
|
||||
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.",
|
||||
github: "https://github.com/KuchtaVR6/nat_coursework",
|
||||
@@ -70,7 +70,7 @@ const projectData : ProjectArguments[] = [
|
||||
SkillEnum.react,
|
||||
SkillEnum.html,
|
||||
SkillEnum.css,
|
||||
SkillEnum.express,
|
||||
SkillEnum.express
|
||||
],
|
||||
github: "https://github.com/KuchtaVR6/Learnopedia"
|
||||
},
|
||||
@@ -148,7 +148,13 @@ const projectData : ProjectArguments[] = [
|
||||
// {
|
||||
// imagePath: "port1.png",
|
||||
// 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: [
|
||||
// SkillEnum.html,
|
||||
// SkillEnum.css,
|
||||
@@ -177,11 +183,17 @@ const projectData : ProjectArguments[] = [
|
||||
SkillEnum.linux,
|
||||
SkillEnum.design3d
|
||||
]
|
||||
},
|
||||
}
|
||||
// {
|
||||
// imagePath: "port3.png",
|
||||
// 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: [
|
||||
// SkillEnum.react,
|
||||
// SkillEnum.html,
|
||||
|
||||
@@ -97,7 +97,7 @@ const workExperienceData : WorkExperienceArgs[] = [
|
||||
country: "Poland",
|
||||
startDate: "July 2018",
|
||||
endDate: "August 2018"
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
export const workExperienceParagraph =
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import { CSSTransition } from "react-transition-group";
|
||||
|
||||
type args = {
|
||||
type AccordionArgs = {
|
||||
title : string,
|
||||
children : React.ReactNode,
|
||||
}
|
||||
|
||||
const Accordion : FC<args> = ({ title, children }) => {
|
||||
const Accordion : FC<AccordionArgs> = ({ title, children }): JSX.Element => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const toggleAccordion = () => {
|
||||
const toggleAccordion = (): void => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export type AwardArgs = {
|
||||
|
||||
const Award : FC<AwardArgs> = (props) => {
|
||||
|
||||
const getCountryEmoji = () => {
|
||||
const getCountryEmoji = (): string => {
|
||||
switch (props.city) {
|
||||
case "Warsaw":
|
||||
return ", PL";
|
||||
|
||||
@@ -14,7 +14,7 @@ export type EducationArgs = {
|
||||
|
||||
const Education : FC<EducationArgs> = (props) => {
|
||||
|
||||
const getCountryEmoji = () => {
|
||||
const getCountryEmoji = (): string => {
|
||||
switch (props.city) {
|
||||
case "Warsaw":
|
||||
return ", Poland";
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { FC, MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
|
||||
|
||||
type args = {
|
||||
type ProgressBarArgs = {
|
||||
percentage : number,
|
||||
customScore? : string,
|
||||
}
|
||||
const ProgressBar : FC<args> = ({percentage,customScore}) => {
|
||||
const ProgressBar : FC<ProgressBarArgs> = ({ percentage,customScore }): JSX.Element => {
|
||||
const ref = useRef<HTMLDivElement>() as MutableRefObject<HTMLDivElement>;
|
||||
const onScreen = useOnScreen(ref);
|
||||
|
||||
@@ -30,7 +29,7 @@ const ProgressBar : FC<args> = ({percentage,customScore}) => {
|
||||
}
|
||||
},25);
|
||||
}
|
||||
},[onScreen]);
|
||||
},[onScreen, current, percentage]);
|
||||
|
||||
return (
|
||||
<div className={"pBContainer"} ref={ref}>
|
||||
@@ -43,7 +42,7 @@ const ProgressBar : FC<args> = ({percentage,customScore}) => {
|
||||
);
|
||||
};
|
||||
|
||||
function useOnScreen(ref : MutableRefObject<HTMLDivElement>, rootMargin = "0px") {
|
||||
function useOnScreen(ref : MutableRefObject<HTMLDivElement>, rootMargin = "0px"): boolean {
|
||||
// State and setter for storing whether element is visible
|
||||
const [isIntersecting, setIntersecting] = useState(false);
|
||||
useEffect(() => {
|
||||
@@ -53,21 +52,24 @@ function useOnScreen(ref : MutableRefObject<HTMLDivElement>, rootMargin = "0px")
|
||||
setIntersecting(entry.isIntersecting);
|
||||
},
|
||||
{
|
||||
rootMargin,
|
||||
rootMargin
|
||||
}
|
||||
);
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
const currentRef = ref.current;
|
||||
if (currentRef) {
|
||||
observer.observe(currentRef);
|
||||
}
|
||||
return () => {
|
||||
if (currentRef) {
|
||||
try {
|
||||
observer.unobserve(ref.current);
|
||||
observer.unobserve(currentRef);
|
||||
}
|
||||
catch {
|
||||
// Observer failure handled silently
|
||||
}
|
||||
catch (e) {
|
||||
console.log("REPORTED: observer failure :/");
|
||||
}
|
||||
};
|
||||
}, []); // Empty array ensures that effect is only run on mount and unmount
|
||||
}, [ref, rootMargin]);
|
||||
return isIntersecting;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import styles from "../styling/projects.module.scss";
|
||||
import { VscGithub } from "react-icons/vsc";
|
||||
import { BsGlobe2, BsFileEarmarkMedical } from "react-icons/bs";
|
||||
|
||||
export interface ProjectArguments {
|
||||
export interface IProjectArguments {
|
||||
imagePath : string,
|
||||
title : string,
|
||||
text : string,
|
||||
@@ -47,7 +47,7 @@ export enum SkillEnum {
|
||||
english = "English",
|
||||
}
|
||||
|
||||
const Project = ({imagePath, title, text, github, access, document} : ProjectArguments) => {
|
||||
const Project = ({ imagePath, title, text, github, access, document } : IProjectArguments): JSX.Element => {
|
||||
return (
|
||||
<div className={styles.projectDisplay}>
|
||||
<div className={styles.text}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import {FC} from "react";
|
||||
import styles from "../styling/projects.module.scss";
|
||||
import { SkillEnum } from "@/src/portfolio/helpers/Project";
|
||||
|
||||
type args = {
|
||||
type SkillDisplayArgs = {
|
||||
name : SkillEnum,
|
||||
level? : ProficiencyLevel,
|
||||
}
|
||||
@@ -15,8 +15,7 @@ export enum ProficiencyLevel {
|
||||
master = "Master",
|
||||
}
|
||||
|
||||
|
||||
const SkillDisplay : FC<args> = ({name, level}) => {
|
||||
const SkillDisplay : FC<SkillDisplayArgs> = ({ name, level }): JSX.Element => {
|
||||
const level_to_emoji = {
|
||||
[ProficiencyLevel.beginner]: "🌱",
|
||||
[ProficiencyLevel.intermediate]: "🌳",
|
||||
|
||||
@@ -13,7 +13,7 @@ export type WorkExperienceArgs = {
|
||||
|
||||
const WorkExperience : FC<WorkExperienceArgs> = (props) => {
|
||||
|
||||
const getEmojiForIndustry = () => {
|
||||
const getEmojiForIndustry = (): string | undefined => {
|
||||
switch (props.industry) {
|
||||
case "Education":
|
||||
return "🎓";
|
||||
|
||||
@@ -4,7 +4,7 @@ import styles from "../styling/achievements.module.scss";
|
||||
import educationData from "@/src/portfolio/data/educationData";
|
||||
import certificateData from "@/src/portfolio/data/certificateData";
|
||||
|
||||
const Achievements = () => {
|
||||
const Achievements = (): JSX.Element => {
|
||||
return (
|
||||
<div className={styles.section} id={"achievements"}>
|
||||
<div>
|
||||
|
||||
@@ -4,8 +4,8 @@ import styles from "../styling/experience.module.scss";
|
||||
import WorkExperience from "@/src/portfolio/helpers/WorkExperience";
|
||||
import workExperienceData, { workExperienceParagraph } from "@/src/portfolio/data/workExperienceData";
|
||||
|
||||
const Experience = () => {
|
||||
const calcPagesOnWidth = (width : number) => {
|
||||
const Experience = (): JSX.Element => {
|
||||
const calcPagesOnWidth = (width : number): number => {
|
||||
return Math.floor(width / 800 + 1);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ const Experience = () => {
|
||||
useEffect(() => {
|
||||
|
||||
setPages(calcPagesOnWidth(window.innerWidth));
|
||||
const handleResize = () => {
|
||||
const handleResize = (): void => {
|
||||
setPages(calcPagesOnWidth(window.innerWidth));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import styles from "../styling/footer.module.scss";
|
||||
|
||||
const Footer = () => {
|
||||
const Footer = (): JSX.Element => {
|
||||
return (
|
||||
<footer className={styles.section}>
|
||||
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!";
|
||||
}
|
||||
};
|
||||
3
src/portfolio/sections/Intro/index.ts
Normal file
3
src/portfolio/sections/Intro/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Intro from "./Intro";
|
||||
|
||||
export default Intro;
|
||||
@@ -3,14 +3,14 @@ import Project from "@/src/portfolio/helpers/Project";
|
||||
import projectData from "@/src/portfolio/data/projectData";
|
||||
import styles from "../styling/projects.module.scss";
|
||||
|
||||
const Projects = () => {
|
||||
const Projects = (): JSX.Element => {
|
||||
return (
|
||||
<div className={styles.section} id={"projects"}>
|
||||
<Splide
|
||||
options={{
|
||||
rewind: true,
|
||||
type: "slide",
|
||||
perPage: 1,
|
||||
perPage: 1
|
||||
}}
|
||||
>
|
||||
{ projectData.map((entry, key) => {
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
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 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;
|
||||
3
src/portfolio/sections/SkillsAndLinks/index.ts
Normal file
3
src/portfolio/sections/SkillsAndLinks/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import SkillsAndLinks from "./SkillsAndLinks";
|
||||
|
||||
export default SkillsAndLinks;
|
||||
29
src/types/images.d.ts
vendored
Normal file
29
src/types/images.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
declare module "*.png" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.jpg" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.jpeg" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.svg" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.gif" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.webp" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
4
src/types/scss.d.ts
vendored
Normal file
4
src/types/scss.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "*.module.scss" {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
@@ -23,6 +23,6 @@
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/types/**/*.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
87
windsurf-commands.sh
Executable file
87
windsurf-commands.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/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 " typecheck - Run TypeScript type checking"
|
||||
echo " security:audit - Run security vulnerability scan"
|
||||
echo " security:check - Generate security report (JSON)"
|
||||
echo " security:outdated - Check for outdated dependencies"
|
||||
echo " help - Show this help message"
|
||||
echo ""
|
||||
}
|
||||
|
||||
run_lint() {
|
||||
echo "🔍 Running ESLint..."
|
||||
pnpm run lint
|
||||
echo "✅ Linting complete!"
|
||||
}
|
||||
|
||||
run_lintfix() {
|
||||
echo "🔧 Running ESLint with auto-fix..."
|
||||
pnpm run lintfix
|
||||
echo "✅ Linting with auto-fix complete!"
|
||||
}
|
||||
|
||||
run_typecheck() {
|
||||
echo "🔎 Running TypeScript type check..."
|
||||
pnpm run typecheck
|
||||
echo "✅ Type checking complete!"
|
||||
}
|
||||
|
||||
run_security_audit() {
|
||||
echo "🔒 Running security audit..."
|
||||
pnpm run security:audit
|
||||
echo "✅ Security audit complete!"
|
||||
}
|
||||
|
||||
run_security_check() {
|
||||
echo "📋 Generating security report..."
|
||||
pnpm run security:check
|
||||
echo "✅ Security report generated: security-report.json"
|
||||
}
|
||||
|
||||
run_security_outdated() {
|
||||
echo "📦 Checking for outdated dependencies..."
|
||||
pnpm run security:outdated
|
||||
echo "✅ Outdated check complete!"
|
||||
}
|
||||
|
||||
# Main script logic
|
||||
case "${1:-help}" in
|
||||
lint)
|
||||
run_lint
|
||||
;;
|
||||
lintfix)
|
||||
run_lintfix
|
||||
;;
|
||||
typecheck)
|
||||
run_typecheck
|
||||
;;
|
||||
security:audit)
|
||||
run_security_audit
|
||||
;;
|
||||
security:check)
|
||||
run_security_check
|
||||
;;
|
||||
security:outdated)
|
||||
run_security_outdated
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
echo "❌ Unknown command: $1"
|
||||
echo ""
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user