Compare commits

..
3 Commits
Author SHA1 Message Date
github-actions[bot] 4217ea11c8 chore: bump api version to 9 [skip ci] 2026-02-27 22:14:21 +00:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>RMCamposrmcampos
f41e34486a Add Source and Copy buttons to note markdown modal (#21)
* Initial plan

* Add Source and Copy buttons to ModalMarkdown for note source code view

Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com>

* feat: add healthcheck do native image

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com>
Co-authored-by: Ricardo Campos <ricardompcampos@gmail.com>
2026-02-27 19:13:53 -03:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>RMCamposrmcampos
29812e8175 fix(readme): replace stale Sonar badges with GitHub Actions CI badges (#15)
* Initial plan

* fix: update README badges - remove Sonar badges and add GitHub Actions CI badges

Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com>

* chore: drop dokploy from backend build

* docs: udpate README

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com>
Co-authored-by: Ricardo Campos <ricardompcampos@gmail.com>
2026-02-27 15:32:31 -03:00
15 changed files with 265 additions and 57 deletions
+2 -4
View File
@@ -71,7 +71,8 @@ jobs:
working-directory: ./server
run: |
./mvnw -Pnative -DskipTests spring-boot:build-image \
-Dspring-boot.build-image.imageName=ghcr.io/${{ steps.repo.outputs.name }}/api:latest
-Dspring-boot.build-image.imageName=ghcr.io/${{ steps.repo.outputs.name }}/api:latest \
-Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:latest
- name: Tag and push Docker image
run: |
@@ -85,6 +86,3 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a api-v${{ steps.version.outputs.version }} -m "Release API v${{ steps.version.outputs.version }}"
git push origin api-v${{ steps.version.outputs.version }}
- name: Trigger Dokploy deployment
run: curl -X POST "${{ vars.DOKPLOY_WEBHOOK_API }}"
+21 -36
View File
@@ -1,21 +1,10 @@
# TaskNote
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
![Build Status](https://github.com/ricardo-campos-org/react-typescript-todolist/actions/workflows/main.yml/badge.svg)
### TaskNote API
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ricardo-campos-org_react-typescript-todolist_server&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_server)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ricardo-campos-org_react-typescript-todolist_server&metric=coverage)](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_server)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=br.com.tasknoteapp%3Aserver&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=br.com.tasknoteapp%3Aserver)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=br.com.tasknoteapp%3Aserver&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=br.com.tasknoteapp%3Aserver)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=br.com.tasknoteapp%3Aserver&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=br.com.tasknoteapp%3Aserver)
### TaskNote WebApp
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ricardo-campos-org_react-typescript-todolist_client&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ricardo-campos-org_react-typescript-todolist_client&metric=coverage)](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=ricardo-campos-org_react-typescript-todolist_client&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ricardo-campos-org_react-typescript-todolist_client&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=ricardo-campos-org_react-typescript-todolist_client&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client)
[![React App CI](https://github.com/RMCampos/tasknote/actions/workflows/client-ci.yml/badge.svg)](https://github.com/RMCampos/tasknote/actions/workflows/client-ci.yml)
[![Server API CI](https://github.com/RMCampos/tasknote/actions/workflows/server-ci.yml/badge.svg)](https://github.com/RMCampos/tasknote/actions/workflows/server-ci.yml)
[![Build and Push App Docker Image](https://github.com/RMCampos/tasknote/actions/workflows/main-client.yml/badge.svg)](https://github.com/RMCampos/tasknote/actions/workflows/main-client.yml)
[![Build and Push API Docker Image](https://github.com/RMCampos/tasknote/actions/workflows/main-server.yml/badge.svg)](https://github.com/RMCampos/tasknote/actions/workflows/main-server.yml)
## 📋 Table of Contents
@@ -49,17 +38,17 @@ The project was born from a month-long technical challenge and has since grown t
- **Responsive Design**: Mobile-first approach with Bootstrap 5 and dark/light theme support
- **Data Visualization**: Task completion charts and productivity analytics
- **File Attachments**: URL attachments for tasks and notes
- **Tagging System**: `#tag` support for better organization
- **Mobile App**: Native mobile applications for iOS and Android with PWA plugin
### Upcoming Features
- **Tagging System**: `#tag` support for better organization
- **Advanced Filters**: Enhanced search with date ranges, priority levels, and status filters
- **Collaboration**: Share tasks and notes with other users
- **Mobile App**: Native mobile applications for iOS and Android
- **Notifications**: Email and push notifications for due dates and reminders
## 🚀 Tech Stack
### Frontend (React TypeScript)
### Frontend (React & TypeScript)
- **Framework**: React 19 with TypeScript for type safety
- **Build Tool**: Vite for fast development and optimized production builds
- **Testing**: Vitest with React Testing Library and comprehensive coverage reporting
@@ -69,8 +58,8 @@ The project was born from a month-long technical challenge and has since grown t
- **Internationalization**: i18next with automatic language detection
- **API Client**: Centralized API service with automatic authentication headers
### Backend (Java Spring Boot)
- **Framework**: Spring Boot 3.5+ with Java 17
### Backend (Java & Spring Boot)
- **Framework**: Spring Boot 4.x.x with Java 25
- **Security**: Spring Security with JWT authentication and refresh tokens
- **Database**: PostgreSQL with JPA/Hibernate ORM
- **Migration**: Flyway for database schema versioning
@@ -84,19 +73,16 @@ The project was born from a month-long technical challenge and has since grown t
- **Containerization**: Docker and Docker Compose for development environment
- **Web Server**: Caddy for reverse proxy and SSL termination
- **CI/CD**: GitHub Actions with automated testing and quality gates
- **Code Analysis**: SonarCloud integration for security and maintainability
## 🏗️ Architecture
### Monorepo Structure
```
react-typescript-todolist/
tasknote/
├── client/ # React TypeScript frontend
├── server/ # Java Spring Boot REST API
├── angular/ # Alternative Angular frontend
├── tools/ # Development and deployment scripts
── docker-compose.yml
└── CLAUDE.md # AI assistant instructions
── docker-compose.yml
```
### Frontend Architecture
@@ -117,15 +103,15 @@ react-typescript-todolist/
### Prerequisites
- **Docker & Docker Compose** (recommended for easy setup)
- **Node.js 18+** and **npm** (for frontend development)
- **Java 17+** and **Maven 3.6+** (for backend development)
- **PostgreSQL 13+** (if running without Docker)
- **Node.js 20+** and **npm** (for frontend development)
- **Java 25+** and **Maven 3.6+** (for backend development)
- **PostgreSQL 15+** (if running without Docker)
### Quick Start with Docker
1. **Clone the repository**
```bash
git clone https://github.com/ricardo-campos-org/react-typescript-todolist.git
cd react-typescript-todolist
git clone https://github.com/rmcampos/tasknote.git
cd tasknote
```
2. **Start the database**
@@ -145,7 +131,6 @@ react-typescript-todolist/
5. **Access the application**
- Frontend: http://localhost:5000
- API Documentation: http://localhost:8080/swagger-ui.html
## 🛠️ Development
@@ -234,7 +219,7 @@ We welcome contributions from the community! This project follows the **Fork & M
1. **Fork the Project** on GitHub
2. **Clone your fork** locally
```bash
git clone https://github.com/YOUR_USERNAME/react-typescript-todolist.git
git clone https://github.com/YOUR_USERNAME/tasknote.git
```
3. **Create a feature branch**
```bash
@@ -269,9 +254,9 @@ For detailed setup instructions and development workflows, see [CONTRIBUTING.md]
**Ricardo Campos** - Full-Stack Developer & Project Maintainer
- **GitHub**: [@ricardo-campos-org](https://github.com/ricardo-campos-org)
- **GitHub**: [@RMCampos](https://github.com/RMCampos)
- **Twitter/X**: [@RMCamposs](https://x.com/RMCamposs)
- **LinkedIn**: [Ricardo Campos](https://www.linkedin.com/in/ricardo-campos-org/)
- **LinkedIn**: [Ricardo Campos](https://www.linkedin.com/in/ricardompcampos/)
### About the Developer
Ricardo is a passionate full-stack developer with expertise in modern web technologies, cloud architecture, and agile development practices. This project showcases his skills in:
@@ -290,8 +275,8 @@ For questions, suggestions, or collaboration opportunities:
- **Email**: Contact via GitHub issues or discussions
- **Twitter/X**: [@RMCamposs](https://x.com/RMCamposs) for quick questions
- **GitHub Issues**: [Create an issue](https://github.com/ricardo-campos-org/react-typescript-todolist/issues) for bugs or feature requests
- **GitHub Discussions**: [Join discussions](https://github.com/ricardo-campos-org/react-typescript-todolist/discussions) for general questions
- **GitHub Issues**: [Create an issue](https://github.com/rmcampos/tasknote/issues) for bugs or feature requests
- **GitHub Discussions**: [Join discussions](https://github.com/rmcampos/tasknote/discussions) for general questions
## 📄 License
+1 -1
View File
@@ -12,7 +12,7 @@ tasks:
docker-build-api:
desc: Build the tasknote-api prod-ready docker image, tagging it as candidate
cmd: cd server && mvn -Pnative -DskipTests spring-boot:build-image -Dspring-boot.build-image.imageName=ghcr.io/rmcampos/tasknote/api:latest
cmd: cd server && mvn -Pnative -DskipTests spring-boot:build-image -Dspring-boot.build-image.imageName=ghcr.io/rmcampos/tasknote/api:latest -Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:latest
prod-up-web:
desc: Speed up the tasknote-web prod-like image, building it if required
@@ -1,6 +1,6 @@
import React from 'react';
import { describe, vi, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, vi, it, expect, beforeEach } from 'vitest';
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react';
import ModalMarkdown from '../../components/ModalMarkdown';
describe('ModalMarkdown Component', () => {
@@ -11,6 +11,14 @@ describe('ModalMarkdown Component', () => {
onHide: vi.fn(),
};
beforeEach(() => {
Object.assign(navigator, {
clipboard: {
writeText: vi.fn().mockResolvedValue(undefined),
},
});
});
it('should render the modal with the correct title and markdown text', () => {
render(<ModalMarkdown {...props} />);
@@ -36,4 +44,64 @@ describe('ModalMarkdown Component', () => {
expect(screen.getByTestId('modal-header-title').innerHTML).toBe('No title');
});
it('should show Source and Copy buttons', () => {
render(<ModalMarkdown {...props} />);
expect(screen.getByTestId('modal-source-button')).toBeDefined();
expect(screen.getByTestId('modal-copy-button')).toBeDefined();
});
it('should toggle to source view when Source button is clicked', () => {
render(<ModalMarkdown {...props} />);
expect(screen.queryByTestId('markdown-source-view')).toBeNull();
fireEvent.click(screen.getByTestId('modal-source-button'));
expect(screen.getByTestId('markdown-source-view')).toBeDefined();
expect(screen.getByTestId('markdown-source-view').textContent).toBe('# Test Markdown');
});
it('should toggle back to rendered view when Source button is clicked again', () => {
render(<ModalMarkdown {...props} />);
fireEvent.click(screen.getByTestId('modal-source-button'));
expect(screen.getByTestId('markdown-source-view')).toBeDefined();
fireEvent.click(screen.getByTestId('modal-source-button'));
expect(screen.queryByTestId('markdown-source-view')).toBeNull();
});
it('should call clipboard writeText with markdownText when Copy button is clicked', async () => {
render(<ModalMarkdown {...props} />);
await act(async () => {
fireEvent.click(screen.getByTestId('modal-copy-button'));
});
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('# Test Markdown');
});
it('should show "Copied!" text after Copy button is clicked', async () => {
render(<ModalMarkdown {...props} />);
await act(async () => {
fireEvent.click(screen.getByTestId('modal-copy-button'));
});
await waitFor(() => {
expect(screen.getByTestId('modal-copy-button').textContent).toBe('Copied!');
});
});
it('should reset source view state when modal is closed', () => {
render(<ModalMarkdown {...props} />);
fireEvent.click(screen.getByTestId('modal-source-button'));
expect(screen.getByTestId('markdown-source-view')).toBeDefined();
fireEvent.click(screen.getByText('Close'));
expect(props.onHide).toHaveBeenCalled();
});
});
+47 -5
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import Button from 'react-bootstrap/Button';
import Modal from 'react-bootstrap/Modal';
import Markdown from 'react-markdown';
@@ -23,11 +23,31 @@ type Props = {
* @returns {React.ReactNode} the Markdown component rendered.
*/
const ModalMarkdown: React.FC<Props> = (props: Props): React.ReactNode => {
const [showSource, setShowSource] = useState<boolean>(false);
const [copied, setCopied] = useState<boolean>(false);
const handleToggleSource = () => setShowSource(prev => !prev);
const handleCopy = () => {
navigator.clipboard.writeText(props.markdownText).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}).catch(() => {
// clipboard write failed silently; no state change
});
};
const handleHide = () => {
setShowSource(false);
setCopied(false);
props.onHide();
};
return props.show
? (
<Modal
show={props.show}
onHide={props.onHide}
onHide={handleHide}
backdrop="static"
keyboard={false}
size="xl"
@@ -42,12 +62,34 @@ const ModalMarkdown: React.FC<Props> = (props: Props): React.ReactNode => {
</Modal.Title>
</Modal.Header>
<Modal.Body className="markdown-modal">
<Markdown remarkPlugins={[remarkGfm]}>{props.markdownText}</Markdown>
{showSource
? (
<pre className="markdown-source" data-testid="markdown-source-view">
{props.markdownText}
</pre>
)
: (
<Markdown remarkPlugins={[remarkGfm]}>{props.markdownText}</Markdown>
)}
</Modal.Body>
<Modal.Footer>
<Button variant="outline-secondary" onClick={props.onHide}>
<Modal.Footer className="d-flex flex-wrap gap-2 justify-content-end">
<Button variant="outline-secondary" onClick={handleHide}>
Close
</Button>
<Button
variant={showSource ? 'info' : 'outline-info'}
onClick={handleToggleSource}
data-testid="modal-source-button"
>
Source
</Button>
<Button
variant="outline-primary"
onClick={handleCopy}
data-testid="modal-copy-button"
>
{copied ? 'Copied!' : 'Copy'}
</Button>
</Modal.Footer>
</Modal>
)
@@ -154,3 +154,24 @@
text-decoration: line-through;
}
}
.markdown-source {
white-space: pre-wrap;
word-break: break-word;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
font-size: 13px;
line-height: 1.6;
padding: 1em;
background-color: var(--bs-tertiary-bg);
border-radius: 6px;
overflow: auto;
max-height: 60vh;
}
@media (max-width: 576px) {
.markdown-source {
font-size: 12px;
padding: 0.75em;
max-height: 50vh;
}
}
+11 -8
View File
@@ -6,13 +6,13 @@ services:
depends_on:
tasknote-api:
condition: service_healthy
image: docker.io/rmcampos/tasknote:app-latest
image: ghcr.io/rmcampos/tasknote/app:latest
build:
context: client
context: ./client
dockerfile: Dockerfile
ports: ["5000:5000"]
environment:
VITE_BACKEND_SERVER: http://tasknote-api:8585
VITE_BACKEND_SERVER: http://localhost:8585
networks:
- tasknote-network
@@ -33,16 +33,19 @@ services:
SECURITY_KEY: this-is-a-very-long-security-key-for-dev
MAILGUN_APIKEY: invalid-api-key-only-placeholder
ports: ["8585:8585"]
image: docker.io/rmcampos/tasknote:api-latest
build:
context: server
dockerfile: Dockerfile
image: ghcr.io/rmcampos/tasknote/api:latest
healthcheck:
test: ["CMD", "/layers/local_healthcheck/healthcheck/bin/healthcheck"]
interval: 10s
timeout: 5s
retries: 3
start_period: 5s
networks:
- tasknote-network
tasknote-db:
container_name: tasknote-db
image: postgres:15.8-bookworm
image: postgres:16
environment:
POSTGRES_DB: tasknote
POSTGRES_USER: tasknoteuser
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build check.go
chmod +x check
mv check buildpacks/healthcheck/healthcheck
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
LAYERS_DIR="$1"
echo "CNB_BUILDPACK_DIR: ${CNB_BUILDPACK_DIR}"
echo "Contents of buildpack dir:"
ls -la "${CNB_BUILDPACK_DIR}"
BINARY_SOURCE="${CNB_BUILDPACK_DIR}/healthcheck"
if [ ! -f "${BINARY_SOURCE}" ]; then
echo "ERROR: binary not found at ${BINARY_SOURCE}"
exit 1
fi
LAYER_DIR="${LAYERS_DIR}/healthcheck"
mkdir -p "${LAYER_DIR}/bin"
cp "${BINARY_SOURCE}" "${LAYER_DIR}/bin/healthcheck"
chmod +x "${LAYER_DIR}/bin/healthcheck"
cat > "${LAYER_DIR}.toml" <<EOL
[types]
launch = true
build = false
cache = false
EOL
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
exit 0
@@ -0,0 +1,11 @@
api = "0.7"
[buildpack]
id = "local/healthcheck"
version = "0.1.0"
name = "Healthcheck Binary Buildpack"
[[stacks]]
id = "io.buildpacks.stacks.jammy"
[[stacks]]
id = "*"
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
package main
import (
"net/http"
"os"
"time"
)
func main() {
client := http.Client{
Timeout: 5 * time.Second,
}
// Use 127.0.0.1 as the healthcheck runs inside the container
resp, err := client.Get("http://127.0.0.1:8585/health")
if err != nil || resp.StatusCode != http.StatusOK {
os.Exit(1)
}
os.Exit(0)
}
+18 -1
View File
@@ -11,7 +11,7 @@
<groupId>br.com.tasknoteapp</groupId>
<artifactId>server</artifactId>
<version>8</version>
<version>9</version>
<name>tasknote-api</name>
<description>Java backend REST API to serve TaskNote frontend client</description>
@@ -113,6 +113,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<!-- Testing -->
<dependency>
@@ -351,6 +359,15 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>ghcr.io/rmcampos/tasknote/api:latest</name>
<buildpacks>
<buildpack>file://${project.basedir}/buildpacks/healthcheck</buildpack>
<buildpack>urn:cnb:builder:paketo-buildpacks/java-native-image</buildpack>
</buildpacks>
</image>
</configuration>
<executions>
<execution>
<goals>
+8
View File
@@ -0,0 +1,8 @@
[build]
builder = "paketobuildpacks/builder-jammy-tiny:latest"
[[build.buildpacks]]
uri = "buildpacks/healthcheck"
[[build.buildpacks]]
uri = "urn:cnb:builder:paketo-buildpacks/java-native-image"