From 34a19a2997719b21306de56c79b095880e613759 Mon Sep 17 00:00:00 2001 From: Mostafa Mirmousavi Date: Tue, 2 Dec 2025 00:51:59 +0100 Subject: [PATCH 1/9] feat: add GH template --- .github/ISSUE_TEMPLATE/bug_report.yml | 94 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 81 +++++++++++++++++++ .github/ISSUE_TEMPLATE/question.yml | 55 +++++++++++++ README.md | 2 +- 5 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/question.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..31f68c9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,94 @@ +name: 🐛 Bug Report +description: Report a bug or unexpected behavior +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug! Please fill out the form below to help us understand and fix the issue. + + - type: dropdown + id: area + attributes: + label: Which area is affected? + description: Which part of the application has the issue? + options: + - Editor + - Presentation Preview + - Login / Sign Up + - Import / Export + - User Dashboard + - AI Features + - UI/UX + - Mostage Library + - Other + validations: + required: true + + - type: textarea + id: description + attributes: + label: Bug Description + description: A clear and concise description of what the bug is. + placeholder: What problem are you experiencing? + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: Please list the steps you took that led to this issue + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. See error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What did you expect to happen? + placeholder: Describe what you expected to happen... + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happened? + placeholder: Describe what actually happened... + validations: + required: true + + - type: textarea + id: screenshots + attributes: + label: Screenshots (Optional) + description: If applicable, add screenshots or images to help explain the problem + placeholder: Drag and drop images here or paste image URLs + + - type: dropdown + id: browser + attributes: + label: Browser + description: Which browser are you using? + options: + - Chrome + - Firefox + - Safari + - Edge + - Other + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context (Optional) + description: Any other information you think might be helpful + placeholder: Add any other context about the problem here... diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..366c850 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: 📚 Documentation + url: https://github.com/mostage-app/studio/tree/main/docs + about: Check out our documentation for setup guides and more information + - name: 💬 Discussions + url: https://github.com/mostage-app/studio/discussions + about: Ask questions and discuss ideas with the community diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..d70550c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,81 @@ +name: ✨ Feature Request +description: Suggest a new feature or improvement +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for suggesting a feature! Please fill out the form below to help us better understand your request. + + - type: dropdown + id: area + attributes: + label: Which area does this feature relate to? + description: Which part of the application would this feature affect? + options: + - Editor + - Presentation Preview + - Login / Sign Up + - Import / Export + - User Dashboard + - AI Features + - UI/UX + - Mostage Library + - Other + validations: + required: true + + - type: textarea + id: problem + attributes: + label: Problem or Need + description: Is this feature related to solving a problem? Please describe. + placeholder: Example: I'm always frustrated when... + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: What feature or improvement would you like? + placeholder: Describe what you would like to see... + validations: + required: true + + - type: textarea + id: use-case + attributes: + label: Use Case + description: How would you use this feature? Who would benefit from it? + placeholder: Describe a specific example or scenario... + validations: + required: false + + - type: textarea + id: mockups + attributes: + label: Mockups or Examples (Optional) + description: If applicable, add images, mockups, or examples + placeholder: Drag and drop images here or paste URLs + + - type: dropdown + id: priority + attributes: + label: Priority (Optional) + description: How important is this feature to you? + options: + - Low - Nice to have + - Medium - Would be helpful + - High - Important for my use case + - Critical - Blocking my workflow + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context (Optional) + description: Any other information you think might be helpful + placeholder: Add any other context, screenshots, or examples here... diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..8026d05 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,55 @@ +name: ❓ Question +description: Ask a question about the website +title: "[Question]: " +labels: ["question"] +body: + - type: markdown + attributes: + value: | + Thanks for your question! We're here to help. Please provide as much detail as possible. + + - type: textarea + id: question + attributes: + label: What is your question? + description: What would you like to know? + placeholder: Ask your question here... + validations: + required: true + + - type: dropdown + id: category + attributes: + label: Question Category + description: Which area does your question relate to? + options: + - Getting Started + - Using the Editor + - Import / Export + - Login / Sign Up + - User Dashboard + - AI Features + - Troubleshooting + - Mostage Library + - Other + validations: + required: true + + - type: textarea + id: context + attributes: + label: Additional Details (Optional) + description: Any additional information that might be helpful + placeholder: | + - What are you trying to accomplish? + - What have you already tried? + - Have you seen any error messages? + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Information (Optional) + description: Any other information that might be helpful + placeholder: Add any other context, screenshots, or examples here... diff --git a/README.md b/README.md index f748664..3521521 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Mostage Studio ![CI Frontend](https://github.com/mostage-app/studio/actions/workflows/ci-frontend.yml/badge.svg) -![CI Frontend](https://github.com/mostage-app/studio/actions/workflows/ci-frontend.yml/badge.svg) +![CI Infrastructure](https://github.com/mostage-app/studio/actions/workflows/ci-infrastructure.yml/badge.svg) ![Next.js](https://img.shields.io/badge/Next.js-15.5.5-black?logo=next.js) ![React](https://img.shields.io/badge/React-19.1.0-blue?logo=react) ![TypeScript](https://img.shields.io/badge/TypeScript-5-blue?logo=typescript) From 4f90874cac2c5119ded80366d69b5df6c31b946b Mon Sep 17 00:00:00 2001 From: Mostafa Mirmousavi Date: Tue, 2 Dec 2025 00:52:23 +0100 Subject: [PATCH 2/9] content: privacy --- frontend/src/app/privacy/page.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/app/privacy/page.tsx b/frontend/src/app/privacy/page.tsx index 3d8944a..7574c2b 100644 --- a/frontend/src/app/privacy/page.tsx +++ b/frontend/src/app/privacy/page.tsx @@ -14,9 +14,6 @@ export default function PrivacyPolicyPage() { {/* Header */}
-
- -

Privacy Policy From 7efea080998a456632260b217d5d86fbef94d5b3 Mon Sep 17 00:00:00 2001 From: Mostafa Mirmousavi Date: Tue, 2 Dec 2025 01:12:09 +0100 Subject: [PATCH 3/9] fix: remove update badge --- .../components/PresentationSettings.tsx | 89 ++----------------- 1 file changed, 5 insertions(+), 84 deletions(-) diff --git a/frontend/src/features/presentation/components/PresentationSettings.tsx b/frontend/src/features/presentation/components/PresentationSettings.tsx index ec8ee90..768a263 100644 --- a/frontend/src/features/presentation/components/PresentationSettings.tsx +++ b/frontend/src/features/presentation/components/PresentationSettings.tsx @@ -4,18 +4,11 @@ import { PresentationToolbarProps, PresentationConfig, } from "../types/presentation.types"; -import { useState, useRef, useEffect } from "react"; +import { useState } from "react"; import { Check, ImagePlus } from "lucide-react"; import { analytics } from "@/lib/utils/analytics"; import { UnsplashImageModal } from "@/features/editor/components/UnsplashImageModal"; -// Indicator component for showing changed tabs -const ChangedIndicator: React.FC = () => { - return ( - - ); -}; - export const PresentationSettings: React.FC = ({ config, onConfigChange, @@ -24,74 +17,6 @@ export const PresentationSettings: React.FC = ({ "general" | "plugins" | "header-footer" | "background" >("general"); - // Store initial config to detect changes (only set on mount) - const initialConfigRef = useRef( - JSON.parse(JSON.stringify(config)) - ); - const [changedTabs, setChangedTabs] = useState>(new Set()); - - // Detect changes in each tab - useEffect(() => { - const tabs: { - name: "general" | "plugins" | "header-footer" | "background"; - checkFn: ( - current: PresentationConfig, - initial: PresentationConfig - ) => boolean; - }[] = [ - { - name: "general", - checkFn: (current, initial) => { - return ( - current.theme !== initial.theme || - current.scale !== initial.scale || - current.loop !== initial.loop || - current.urlHash !== initial.urlHash || - JSON.stringify(current.transition) !== - JSON.stringify(initial.transition) || - JSON.stringify(current.centerContent) !== - JSON.stringify(initial.centerContent) - ); - }, - }, - { - name: "header-footer", - checkFn: (current, initial) => { - return ( - JSON.stringify(current.header) !== JSON.stringify(initial.header) || - JSON.stringify(current.footer) !== JSON.stringify(initial.footer) - ); - }, - }, - { - name: "background", - checkFn: (current, initial) => { - return ( - JSON.stringify(current.background) !== - JSON.stringify(initial.background) - ); - }, - }, - { - name: "plugins", - checkFn: (current, initial) => { - return ( - JSON.stringify(current.plugins) !== JSON.stringify(initial.plugins) - ); - }, - }, - ]; - - const newChangedTabs = new Set(); - tabs.forEach((tab) => { - if (tab.checkFn(config, initialConfigRef.current)) { - newChangedTabs.add(tab.name); - } - }); - - setChangedTabs(newChangedTabs); - }, [config]); - // Handle tab change with analytics tracking const handleTabChange = ( tabName: "general" | "plugins" | "header-footer" | "background" @@ -226,47 +151,43 @@ export const PresentationSettings: React.FC = ({
From 706817bb852cb2850563df281635edadeb3ff0db Mon Sep 17 00:00:00 2001 From: Mostafa Mirmousavi Date: Tue, 2 Dec 2025 01:12:22 +0100 Subject: [PATCH 4/9] content: update plan message --- frontend/src/app/[username]/page.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/[username]/page.tsx b/frontend/src/app/[username]/page.tsx index 27f04f7..0b31148 100644 --- a/frontend/src/app/[username]/page.tsx +++ b/frontend/src/app/[username]/page.tsx @@ -20,6 +20,7 @@ import { Check, X, MonitorPlay, + Heart, } from "lucide-react"; import { deletePresentation, @@ -537,8 +538,22 @@ export default function UserProfilePage() {

Currently, you cannot upgrade your plan without a referral - link.
+ link. +
+
In March 2026, the other plans will be available publicly. +
+
+ You can{" "} + + donate + {" "} + for supporting faster development.

)} From 3d8f5551b75664fbcdbc6994d81407810c41bc01 Mon Sep 17 00:00:00 2001 From: Mostafa Mirmousavi Date: Tue, 2 Dec 2025 01:24:57 +0100 Subject: [PATCH 5/9] ci: add doc for ci and improve template --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/workflows/ci-backend.yml | 61 +++++++++++++ .github/workflows/ci-infrastructure.yml | 2 +- README.md | 17 +++- backend/README.md | 23 ++++- docs/ci-cd.md | 101 +++++++-------------- 6 files changed, 132 insertions(+), 74 deletions(-) create mode 100644 .github/workflows/ci-backend.yml diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index d70550c..1d03665 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -31,7 +31,7 @@ body: attributes: label: Problem or Need description: Is this feature related to solving a problem? Please describe. - placeholder: Example: I'm always frustrated when... + placeholder: "Example: I'm always frustrated when..." validations: required: true diff --git a/.github/workflows/ci-backend.yml b/.github/workflows/ci-backend.yml new file mode 100644 index 0000000..9041636 --- /dev/null +++ b/.github/workflows/ci-backend.yml @@ -0,0 +1,61 @@ +name: CI Backend + +on: + push: + branches: [main, dev] + paths: + - "backend/**" + - ".github/workflows/ci-backend.yml" + pull_request: + branches: [main, dev] + paths: + - "backend/**" + - ".github/workflows/ci-backend.yml" + +permissions: + contents: read + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: "22" + cache: "npm" + cache-dependency-path: backend/package-lock.json + + - name: Install dependencies + working-directory: ./backend + run: npm ci + + - name: Build project + working-directory: ./backend + run: npm run build + + type-check: + name: Type Check + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: "22" + cache: "npm" + cache-dependency-path: backend/package-lock.json + + - name: Install dependencies + working-directory: ./backend + run: npm ci + + - name: Type check + working-directory: ./backend + run: npx tsc --noEmit diff --git a/.github/workflows/ci-infrastructure.yml b/.github/workflows/ci-infrastructure.yml index b10d39a..e3e3281 100644 --- a/.github/workflows/ci-infrastructure.yml +++ b/.github/workflows/ci-infrastructure.yml @@ -38,7 +38,7 @@ jobs: working-directory: ./infrastructure run: npm run build - lint: + type-check: name: Type Check runs-on: ubuntu-latest steps: diff --git a/README.md b/README.md index 3521521..3e0e3a0 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,22 @@ # Mostage Studio ![CI Frontend](https://github.com/mostage-app/studio/actions/workflows/ci-frontend.yml/badge.svg) +![CI Backend](https://github.com/mostage-app/studio/actions/workflows/ci-backend.yml/badge.svg) ![CI Infrastructure](https://github.com/mostage-app/studio/actions/workflows/ci-infrastructure.yml/badge.svg) + ![Next.js](https://img.shields.io/badge/Next.js-15.5.5-black?logo=next.js) -![React](https://img.shields.io/badge/React-19.1.0-blue?logo=react) -![TypeScript](https://img.shields.io/badge/TypeScript-5-blue?logo=typescript) -![AWS CDK](https://img.shields.io/badge/AWS%20CDK-2.100.0-orange?logo=aws) +![React](https://img.shields.io/badge/React-19.1.0-61DAFB?logo=react&logoColor=white) +![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white) +![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-4-38B2AC?logo=tailwind-css&logoColor=white) +![Node.js](https://img.shields.io/badge/Node.js-20-339933?logo=node.js&logoColor=white) + +![AWS CDK](https://img.shields.io/badge/AWS%20CDK-2.100.0-FF9900?logo=amazon-aws&logoColor=white) +![AWS Lambda](https://img.shields.io/badge/AWS%20Lambda-FF9900?logo=aws-lambda&logoColor=white) +![AWS DynamoDB](https://img.shields.io/badge/AWS%20DynamoDB-FF9900?logo=amazon-dynamodb&logoColor=white) +![AWS Cognito](https://img.shields.io/badge/AWS%20Cognito-FF9900?logo=amazon-aws&logoColor=white) +![AWS API Gateway](https://img.shields.io/badge/AWS%20API%20Gateway-FF9900?logo=amazon-api-gateway&logoColor=white) +![AWS S3](https://img.shields.io/badge/AWS%20S3-FF9900?logo=amazon-s3&logoColor=white) +![AWS Amplify](https://img.shields.io/badge/AWS%20Amplify-FF9900?logo=aws-amplify&logoColor=white) Mostage Studio is a simple online tool for making presentations with Markdown and HTML. Some features include AI Creation, Live Polling System, and Audience Q&A. diff --git a/backend/README.md b/backend/README.md index 5d0d26f..4105887 100644 --- a/backend/README.md +++ b/backend/README.md @@ -4,7 +4,7 @@ Backend Lambda functions for Mostage Studio. ## Structure -``` +```text backend/ src/ lambda/ @@ -93,3 +93,24 @@ Watch mode: ```bash npm run watch ``` + +## CI/CD + +The backend has automated CI checks via GitHub Actions. The CI workflow runs on: + +- Push to `main` or `dev` branches when files in `backend/` change +- Pull requests to `main` or `dev` branches when files in `backend/` change + +**CI Jobs**: + +- **Build**: Compiles TypeScript to JavaScript +- **Type Check**: Validates TypeScript types + +To run CI checks locally: + +```bash +npm run build +npx tsc --noEmit +``` + +See [CI/CD Documentation](../docs/ci-cd.md) for more details. diff --git a/docs/ci-cd.md b/docs/ci-cd.md index f936d54..5ed7ee1 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -4,13 +4,13 @@ This document describes the Continuous Integration and Continuous Deployment (CI ## Overview -The project uses GitHub Actions for automated testing, building, and deployment. There are three main workflows: +The project uses GitHub Actions for automated testing, building, and deployment. There are three main CI workflows: 1. **CI Frontend Workflow** - Runs on frontend changes -2. **CI Infrastructure Workflow** - Runs on infrastructure changes -3. **Deploy Frontend Workflow** - Deploys frontend to GitHub Pages +2. **CI Backend Workflow** - Runs on backend changes +3. **CI Infrastructure Workflow** - Runs on infrastructure changes -**Note**: Infrastructure deployment is performed **manually** using AWS CDK commands locally. See [Infrastructure Setup](infrastructure.md) for deployment instructions. +**Note**: Infrastructure deployment is performed **manually** using AWS CDK commands locally. Frontend deployment is handled by AWS Amplify (see `amplify.yml`). See [Infrastructure Setup](infrastructure.md) for deployment instructions. ## Workflows @@ -47,75 +47,39 @@ The project uses GitHub Actions for automated testing, building, and deployment. **Location**: `.github/workflows/ci-infrastructure.yml` -### 3. Deploy Frontend Workflow (`deploy-frontend.yml`) +### 3. CI Backend Workflow (`ci-backend.yml`) -**Purpose**: Deploys the frontend application to GitHub Pages +**Purpose**: Automated validation for backend Lambda functions **Triggers**: -- Push to `main` branch **only when files in `frontend/` change** -- Manual trigger via `workflow_dispatch` +- Push to `main` or `dev` branches **only when files in `backend/` change** +- Pull requests to `main` or `dev` branches **only when files in `backend/` change** -**Process**: - -1. Installs dependencies -2. Builds the Next.js application (lint and type-check are handled by CI workflow) -3. Uploads build artifacts -4. Deploys to GitHub Pages - -**Note**: This workflow only builds and deploys. Lint and type-check are performed by the CI Frontend workflow to avoid duplicate work. - -**Environment Variables**: - -- `NEXT_PUBLIC_GA_MEASUREMENT_ID` (optional) - Google Analytics ID -- `NEXT_PUBLIC_COGNITO_USER_POOL_ID_PROD` (required) - Production Cognito User Pool ID -- `NEXT_PUBLIC_COGNITO_CLIENT_ID_PROD` (required) - Production Cognito Client ID -- `NEXT_PUBLIC_AWS_REGION` (required) - AWS Region (e.g., `eu-central-1`) -- `NEXT_PUBLIC_API_URL` (required) - API Gateway URL for backend services +**Jobs**: -**Note**: These environment variables must be set in GitHub Secrets (Settings → Secrets and variables → Actions) because Next.js requires them at build time for static export. +- **Build**: Compiles TypeScript to JavaScript +- **Type Check**: Validates TypeScript types -**Location**: `.github/workflows/deploy-frontend.yml` +**Location**: `.github/workflows/ci-backend.yml` ## GitHub Setup ### Required Secrets -Go to **Settings → Secrets and variables → Actions** and add: - -#### For Frontend Deploy - -- `NEXT_PUBLIC_GA_MEASUREMENT_ID` (optional) - Google Analytics Measurement ID -- `NEXT_PUBLIC_COGNITO_USER_POOL_ID_PROD` (required) - Production Cognito User Pool ID -- `NEXT_PUBLIC_COGNITO_CLIENT_ID_PROD` (required) - Production Cognito Client ID -- `NEXT_PUBLIC_AWS_REGION` (required) - AWS Region (e.g., `eu-central-1`) -- `NEXT_PUBLIC_API_URL` (required) - API Gateway URL for backend services - -### Required Environments - -Go to **Settings → Environments** and create: - -#### `github-pages` - -- Usually created automatically -- Used for frontend deployment - -### GitHub Pages Configuration - -Go to **Settings → Pages**: - -- **Source**: Select "GitHub Actions" -- **Branch**: `main` (or your default branch) +Currently, no GitHub Secrets are required for CI workflows. Environment variables for frontend builds are handled by AWS Amplify (see `amplify.yml`). ## Workflow Files ```text .github/workflows/ ├── ci-frontend.yml # Frontend CI checks (lint, type-check, build) -├── ci-infrastructure.yml # Infrastructure CI checks (validate, format) -└── deploy-frontend.yml # Frontend deployment to GitHub Pages +├── ci-backend.yml # Backend CI checks (build, type-check) +└── ci-infrastructure.yml # Infrastructure CI checks (build, type-check) ``` +**Note**: Frontend deployment is handled by AWS Amplify (see `amplify.yml` in the root directory). + ## Usage ### Running CI Checks @@ -123,20 +87,14 @@ Go to **Settings → Pages**: CI checks run automatically on: - **Frontend CI**: When files in `frontend/` change +- **Backend CI**: When files in `backend/` change - **Infrastructure CI**: When files in `infrastructure/` change -You can also manually trigger from **Actions → CI Frontend** or **Actions → CI Infrastructure** +You can also manually trigger from **Actions → CI Frontend**, **Actions → CI Backend**, or **Actions → CI Infrastructure** ### Deploying Frontend -Frontend deploys automatically when you push to `main` branch. - -To deploy manually: - -1. Go to **Actions → Deploy Frontend** -2. Click **Run workflow** -3. Select branch (usually `main`) -4. Click **Run workflow** +Frontend deployment is handled by AWS Amplify. See `amplify.yml` in the root directory for configuration. ### Deploying Infrastructure @@ -202,24 +160,31 @@ See [Infrastructure Setup](infrastructure.md) for detailed instructions. - Check type check output in Actions logs - Type check locally: `cd infrastructure && npx tsc --noEmit` -### Deploy Frontend Fails +### CI Backend Workflow Fails **Build errors**: -- Check if environment variables are set correctly -- Verify `NEXT_PUBLIC_GA_MEASUREMENT_ID` is set (if using analytics) +- Check build output in Actions logs +- Build locally: `cd backend && npm run build` -**Deployment errors**: +**TypeScript type errors**: -- Check GitHub Pages settings -- Verify Pages source is set to "GitHub Actions" +- Check type check output in Actions logs +- Type check locally: `cd backend && npx tsc --noEmit` ## Best Practices 1. **Always run CI locally** before pushing: ```bash + # Frontend cd frontend && npm run lint && npx tsc --noEmit && npm run build + + # Backend + cd backend && npm run build && npx tsc --noEmit + + # Infrastructure + cd infrastructure && npm run build && npx tsc --noEmit ``` 2. **Review CI results** before merging PRs From fdcd80a25ddb70d419464566a62802694766485b Mon Sep 17 00:00:00 2001 From: Mostafa Mirmousavi Date: Tue, 2 Dec 2025 06:58:42 +0100 Subject: [PATCH 6/9] fix: improve save message --- frontend/src/lib/common/SaveButton.tsx | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/common/SaveButton.tsx b/frontend/src/lib/common/SaveButton.tsx index f7037e7..db3ef72 100644 --- a/frontend/src/lib/common/SaveButton.tsx +++ b/frontend/src/lib/common/SaveButton.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState, useEffect } from "react"; import { Save, Loader2, Check, AlertCircle } from "lucide-react"; // Auto-save state interface @@ -28,6 +29,22 @@ export function SaveButton({ isAuthenticated, onLoginRequired, }: SaveButtonProps) { + const [formattedTime, setFormattedTime] = useState(""); + const [isMounted, setIsMounted] = useState(false); + + // Only format time on client side to avoid hydration mismatch + useEffect(() => { + setIsMounted(true); + if (autoSaveState?.lastSaved) { + setFormattedTime( + autoSaveState.lastSaved.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }) + ); + } + }, [autoSaveState?.lastSaved]); + const handleSaveClick = () => { if (!isAuthenticated) { onLoginRequired(); @@ -80,11 +97,7 @@ export function SaveButton({
- Saved{" "} - {autoSaveState.lastSaved.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - })} + Saved{isMounted && formattedTime ? ` ${formattedTime}` : ""}
) : autoSaveState.hasUnsavedChanges ? ( From a8ad9d17f665adc4da7199cf3d65fe69456fbcb9 Mon Sep 17 00:00:00 2001 From: Mostafa Mirmousavi Date: Tue, 2 Dec 2025 07:33:58 +0100 Subject: [PATCH 7/9] feat: share profile and presentations --- frontend/src/app/[username]/page.tsx | 617 +++++++----------- frontend/src/features/index.ts | 1 + .../profile/components/PresentationCard.tsx | 161 +++++ .../profile/components/PresentationsGrid.tsx | 90 +++ .../profile/components/ProfileCard.tsx | 252 +++++++ .../features/profile/components/ShareMenu.tsx | 89 +++ .../profile/components/ShareProfileBox.tsx | 124 ++++ .../components/SharedPresentationsGrid.tsx | 84 +++ .../src/features/profile/components/index.ts | 6 + frontend/src/features/profile/index.ts | 3 + frontend/src/features/profile/types/index.ts | 1 + .../features/profile/types/profile.types.ts | 20 + frontend/src/features/profile/utils/index.ts | 1 + .../features/profile/utils/profile.utils.ts | 45 ++ 14 files changed, 1110 insertions(+), 384 deletions(-) create mode 100644 frontend/src/features/profile/components/PresentationCard.tsx create mode 100644 frontend/src/features/profile/components/PresentationsGrid.tsx create mode 100644 frontend/src/features/profile/components/ProfileCard.tsx create mode 100644 frontend/src/features/profile/components/ShareMenu.tsx create mode 100644 frontend/src/features/profile/components/ShareProfileBox.tsx create mode 100644 frontend/src/features/profile/components/SharedPresentationsGrid.tsx create mode 100644 frontend/src/features/profile/components/index.ts create mode 100644 frontend/src/features/profile/index.ts create mode 100644 frontend/src/features/profile/types/index.ts create mode 100644 frontend/src/features/profile/types/profile.types.ts create mode 100644 frontend/src/features/profile/utils/index.ts create mode 100644 frontend/src/features/profile/utils/profile.utils.ts diff --git a/frontend/src/app/[username]/page.tsx b/frontend/src/app/[username]/page.tsx index 0b31148..c9dba55 100644 --- a/frontend/src/app/[username]/page.tsx +++ b/frontend/src/app/[username]/page.tsx @@ -1,27 +1,13 @@ "use client"; -import { useAuthContext } from "@/features/auth/components/AuthProvider"; +// React & Next.js +import { useEffect, useState, useCallback, useRef } from "react"; import { useParams } from "next/navigation"; -import Image from "next/image"; -import { - FileText, - Globe, - Calendar, - Lock, - Loader2, - Pencil, - Trash2, - Link as LinkIcon, - User, - Mail, - Package, - Settings, - Edit2, - Check, - X, - MonitorPlay, - Heart, -} from "lucide-react"; +import { Loader2, Trash2 } from "lucide-react"; + +// Features +import { useAuthContext } from "@/features/auth/components/AuthProvider"; +import { AuthService } from "@/features/auth/services/authService"; import { deletePresentation, getPresentations, @@ -29,51 +15,69 @@ import { type Presentation, } from "@/features/presentation/services/presentationService"; import { EditPresentationModal } from "@/features/presentation/components/EditPresentationModal"; -import Link from "next/link"; -import { useEffect, useState, useCallback } from "react"; + +// Components import { Modal } from "@/lib/components/ui/Modal"; -import { AuthService } from "@/features/auth/services/authService"; import { NotFoundPage } from "@/lib/components/NotFoundPage"; -import CryptoJS from "crypto-js"; - -/** - * Generate Gravatar URL from email - */ -function getGravatarUrl(email: string, size: number = 96): string { - // Normalize email: lowercase and trim - const normalizedEmail = email.toLowerCase().trim(); - - // Generate MD5 hash using crypto-js - const emailHash = CryptoJS.MD5(normalizedEmail).toString(); - - // Gravatar URL format: https://www.gravatar.com/avatar/{hash}?s={size}&d={default} - return `https://www.gravatar.com/avatar/${emailHash}?s=${size}&d=identicon&r=pg`; -} +import { + ProfileCard, + ShareProfileBox, + PresentationsGrid, + SharedPresentationsGrid, +} from "@/features/profile"; +import type { + ProfileUser, + DeleteModalState, + EditModalState, + SharePlatform, +} from "@/features/profile"; +import { COPY_FEEDBACK_DURATION, getGravatarUrl } from "@/features/profile"; + +// ============================================================================ +// Main Component +// ============================================================================ export default function UserProfilePage() { + // ======================================================================== + // Hooks & Context + // ======================================================================== const params = useParams(); const username = params?.username as string; - const { user, isAuthenticated, updateUser } = useAuthContext(); + + // ======================================================================== + // State Management + // ======================================================================== + + // Presentations const [presentations, setPresentations] = useState([]); const [isLoading, setIsLoading] = useState(true); - // Note: presentationError is kept for potential future use in displaying presentation fetch errors + // Note: presentationError is kept for potential future use // eslint-disable-next-line @typescript-eslint/no-unused-vars const [presentationError, setPresentationError] = useState( null ); - const [deleteModal, setDeleteModal] = useState<{ - isOpen: boolean; - slug: string; - name: string; - }>({ isOpen: false, slug: "", name: "" }); + + // Shared presentations (presentations shared with the user) + // Note: setSharedPresentations will be used when backend API is implemented + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [sharedPresentations, setSharedPresentations] = useState< + Presentation[] + >([]); + + // Presentation modals + const [deleteModal, setDeleteModal] = useState({ + isOpen: false, + slug: "", + name: "", + }); const [isDeleting, setIsDeleting] = useState(false); - const [editModal, setEditModal] = useState<{ - isOpen: boolean; - presentation: Presentation | null; - }>({ isOpen: false, presentation: null }); + const [editModal, setEditModal] = useState({ + isOpen: false, + presentation: null, + }); - // Profile editing states + // Profile editing const [isEditingName, setIsEditingName] = useState(false); const [editedName, setEditedName] = useState(""); const [isSavingName, setIsSavingName] = useState(false); @@ -81,14 +85,22 @@ export default function UserProfilePage() { const [profileSuccess, setProfileSuccess] = useState(""); const [showUpgradeMessage, setShowUpgradeMessage] = useState(false); - // State for other users' profile data (only name and username) - const [profileUser, setProfileUser] = useState<{ - name?: string; - username: string; - } | null>(null); + // Other user's profile + const [profileUser, setProfileUser] = useState(null); const [userNotFound, setUserNotFound] = useState(false); const [gravatarUrl, setGravatarUrl] = useState(null); + // Sharing + const [linkCopied, setLinkCopied] = useState(false); + const [shareMenuOpen, setShareMenuOpen] = useState(null); + const [presentationLinkCopied, setPresentationLinkCopied] = useState< + string | null + >(null); + const shareMenuRefs = useRef>({}); + + // ======================================================================== + // Computed Values + // ======================================================================== const isOwnProfile = isAuthenticated && user?.username === username; // Generate Gravatar URL for own profile @@ -164,6 +176,7 @@ export default function UserProfilePage() { setProfileUser({ name: data.name, username: data.username, + createdAt: data.createdAt, }); } } catch (error) { @@ -254,28 +267,9 @@ export default function UserProfilePage() { [editModal.presentation, username] ); - const formatDate = (dateString: string) => { - const date = new Date(dateString); - return date.toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); - }; - - const formatFullDate = (dateString?: string): string => { - if (!dateString) return "N/A"; - try { - const date = new Date(dateString); - return date.toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - }); - } catch { - return "N/A"; - } - }; + // ======================================================================== + // Event Handlers + // ======================================================================== // Profile editing handlers const handleStartEditName = () => { @@ -328,6 +322,113 @@ export default function UserProfilePage() { setShowUpgradeMessage((prev) => !prev); }; + const handleCopyLink = useCallback(async () => { + const profileUrl = `${window.location.origin}/${username}`; + const shareText = `Check out my presentations on Mostage! #mostage #presentation `; + const fullText = `${shareText}${profileUrl}`; + try { + await navigator.clipboard.writeText(fullText); + setLinkCopied(true); + setTimeout(() => setLinkCopied(false), COPY_FEEDBACK_DURATION); + } catch (err) { + console.error("Failed to copy link:", err); + } + }, [username]); + + const handleShare = useCallback( + (platform: Exclude) => { + const profileUrl = `${window.location.origin}/${username}`; + + // Professional sharing text + const shareText = `Check out my presentations on Mostage! #mostage #presentation `; + const shareTextEncoded = encodeURIComponent(shareText); + const profileUrlEncoded = encodeURIComponent(profileUrl); + + const urls: Record = { + twitter: `https://twitter.com/intent/tweet?url=${profileUrlEncoded}&text=${shareTextEncoded}`, + facebook: `https://www.facebook.com/sharer.php?u=${profileUrl}`, + linkedin: `https://www.linkedin.com/feed/?shareActive&mini=true&text=${shareTextEncoded}${profileUrlEncoded}`, + }; + + window.open(urls[platform], "_blank", "width=600,height=400"); + }, + [username] + ); + + const handleSharePresentation = useCallback( + (slug: string, name: string, platform?: SharePlatform) => { + const presentationUrl = `${window.location.origin}/${username}/${slug}/view`; + const shareText = `Check out "${name}" presentation on Mostage! #mostage #presentation `; + const shareTextEncoded = encodeURIComponent(shareText); + const presentationUrlEncoded = encodeURIComponent(presentationUrl); + + // Handle copy to clipboard + if (platform === "copy") { + const fullText = `${shareText}${presentationUrl}`; + navigator.clipboard.writeText(fullText).then(() => { + setPresentationLinkCopied(slug); + setTimeout( + () => setPresentationLinkCopied(null), + COPY_FEEDBACK_DURATION + ); + }); + setShareMenuOpen(null); + return; + } + + // Handle social platform sharing + if (platform) { + const urls: Record, string> = { + twitter: `https://twitter.com/intent/tweet?url=${presentationUrlEncoded}&text=${shareTextEncoded}`, + facebook: `https://www.facebook.com/sharer.php?u=${presentationUrlEncoded}`, + linkedin: `https://www.linkedin.com/feed/?shareActive&mini=true&text=${shareTextEncoded}&url=${presentationUrlEncoded}`, + }; + + window.open(urls[platform], "_blank", "width=600,height=400"); + setShareMenuOpen(null); + return; + } + + // Try Web Share API first (works on mobile and some desktop browsers) + if (navigator.share) { + navigator + .share({ + title: `${name} - Mostage`, + text: shareText, + url: presentationUrl, + }) + .catch(() => { + // User cancelled or error occurred + }); + setShareMenuOpen(null); + return; + } + + // Default: open share menu + setShareMenuOpen(slug); + }, + [username] + ); + + // Close share menu when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (shareMenuOpen) { + const menuElement = shareMenuRefs.current[shareMenuOpen]; + if (menuElement && !menuElement.contains(event.target as Node)) { + setShareMenuOpen(null); + } + } + }; + + if (shareMenuOpen) { + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + } + }, [shareMenuOpen]); + const displayedPresentations = isOwnProfile ? presentations : presentations.filter((p) => p.isPublic); @@ -362,316 +463,64 @@ export default function UserProfilePage() {
{/* Left Sidebar - Profile Info */}
-
- {/* Avatar */} -
- {isOwnProfile && gravatarUrl ? ( -
- {user?.name setGravatarUrl(null)} - unoptimized - /> -
- ) : ( -
- -
- )} -
- - {/* Messages */} - {profileSuccess && ( -
-

- {profileSuccess} -

-
- )} - - {profileError && ( -
-

- {profileError} -

-
- )} - - {/* Name */} -
- {isOwnProfile && isEditingName ? ( -
- setEditedName(e.target.value)} - onKeyDown={(e) => { - if ( - e.key === "Enter" && - editedName.trim() && - !isSavingName - ) { - handleSaveName(); - } else if (e.key === "Escape") { - handleCancelEditName(); - } - }} - className="text-xl font-semibold text-foreground bg-transparent border-none outline-none text-center focus:ring-0 focus:border-b-2 focus:border-primary px-1 py-0.5 min-w-0 flex-1 max-w-xs disabled:opacity-50" - placeholder="Enter your name" - disabled={isSavingName} - autoFocus - /> - - -
- ) : ( -
-

- {isOwnProfile - ? user?.name || user?.username - : profileUser?.name || username} -

- {isOwnProfile && ( - - )} -
- )} -
- - {/* User Details */} -
- {/* Username */} -
- -
-

Username

-

- @{username} -

-
-
- - {/* Email - only for own profile */} - {isOwnProfile && user?.email && ( -
- -
-

Email

-

- {user.email} -

-
-
- )} - - {/* Member Since */} - {isOwnProfile && ( -
- -
-

- Member Since -

-

- {formatFullDate(user?.createdAt)} -

-
-
- )} - - {/* Subscription - only for own profile */} - {isOwnProfile && ( -
- -
-

Plan

-

Basic Plan

-
-
- )} -
- - {/* Account Actions - only for own profile */} - {isOwnProfile && ( -
- -
- )} - - {showUpgradeMessage && ( -
-

- Currently, you cannot upgrade your plan without a referral - link. -
-
- In March 2026, the other plans will be available publicly. -
-
- You can{" "} - - donate - {" "} - for supporting faster development. -

-
- )} -
+ setGravatarUrl(null)} + /> + + {/* Share Profile Box - only for own profile */} + {isOwnProfile && ( + + )}
{/* Right Content - Presentations */}
- {displayedPresentations.length > 0 ? ( -
- {displayedPresentations.map((pres) => ( -
-
-
- -

- {pres.name} -

-
-
- - {/* Slug */} -
- - {pres.slug} -
- -
- {pres.isPublic ? ( -
- - Public -
- ) : ( -
- - Private -
- )} -
- -
-
- - Created: {formatDate(pres.createdAt)} -
-
- - Updated: {formatDate(pres.updatedAt)} -
-
- - {/* Action buttons */} -
- {/* View button */} - - - {isOwnProfile && ( - <> - {/* Edit Content button */} - - - Edit - - - {/* Settings button */} - - - {/* Delete button */} - - - )} -
-
- ))} -
- ) : ( -
- -

- {isOwnProfile - ? "No presentations yet" - : "No public presentations"} -

-

- {isOwnProfile - ? "Create your first presentation to get started" - : `${username} hasn't shared any public presentations yet`} -

-
+ + + {/* Shared Presentations - only for own profile */} + {isOwnProfile && ( + )}
diff --git a/frontend/src/features/index.ts b/frontend/src/features/index.ts index 1b382bf..05f94eb 100644 --- a/frontend/src/features/index.ts +++ b/frontend/src/features/index.ts @@ -4,3 +4,4 @@ export * from "./import"; export * from "./export"; export * from "./auth"; export * from "./app-info"; +export * from "./profile"; diff --git a/frontend/src/features/profile/components/PresentationCard.tsx b/frontend/src/features/profile/components/PresentationCard.tsx new file mode 100644 index 0000000..50e71d7 --- /dev/null +++ b/frontend/src/features/profile/components/PresentationCard.tsx @@ -0,0 +1,161 @@ +"use client"; + +import Link from "next/link"; +import { + FilePlay, + Link as LinkIcon, + Globe, + Lock, + Calendar, + MonitorPlay, + Pencil, + Settings, + Trash2, + Share2, +} from "lucide-react"; +import type { Presentation } from "@/features/presentation/services/presentationService"; +import type { SharePlatform } from "../types"; +import { formatDate } from "../utils"; +import { ShareMenu } from "./ShareMenu"; + +interface PresentationCardProps { + presentation: Presentation; + username: string; + isOwnProfile: boolean; + shareMenuOpen: string | null; + presentationLinkCopied: string | null; + onShare: (slug: string, name: string, platform?: SharePlatform) => void; + onView: (slug: string) => void; + onEdit: (presentation: Presentation) => void; + onDelete: (slug: string, name: string) => void; + menuRef: (el: HTMLDivElement | null) => void; +} + +export function PresentationCard({ + presentation: pres, + username, + isOwnProfile, + shareMenuOpen, + presentationLinkCopied, + onShare, + onView, + onEdit, + onDelete, + menuRef, +}: PresentationCardProps) { + return ( +
+ {/* Header with Share button */} +
+
+ +

+ {pres.name} +

+
+ + {/* Share button with dropdown */} +
+ + + +
+
+ + {/* Slug */} +
+ + {pres.slug} +
+ + {/* Visibility Badge */} +
+ {pres.isPublic ? ( +
+ + Public +
+ ) : ( +
+ + Private +
+ )} +
+ + {/* Date Information */} +
+
+ + + Created: {formatDate(pres.createdAt)} + +
+
+ + + Updated: {formatDate(pres.updatedAt)} + +
+
+ + {/* Action buttons */} +
+ {/* View button */} + + + {isOwnProfile && ( + <> + {/* Edit Content button */} + + + Edit + + + {/* Settings button */} + + + {/* Delete button */} + + + )} +
+
+ ); +} diff --git a/frontend/src/features/profile/components/PresentationsGrid.tsx b/frontend/src/features/profile/components/PresentationsGrid.tsx new file mode 100644 index 0000000..7c19f88 --- /dev/null +++ b/frontend/src/features/profile/components/PresentationsGrid.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { Files, FileText } from "lucide-react"; +import type { Presentation } from "@/features/presentation/services/presentationService"; +import type { SharePlatform } from "../types"; +import { PresentationCard } from "./PresentationCard"; + +interface PresentationsGridProps { + presentations: Presentation[]; + username: string; + isOwnProfile: boolean; + shareMenuOpen: string | null; + presentationLinkCopied: string | null; + onShare: (slug: string, name: string, platform?: SharePlatform) => void; + onView: (slug: string) => void; + onEdit: (presentation: Presentation) => void; + onDelete: (slug: string, name: string) => void; + menuRefs: Record; +} + +export function PresentationsGrid({ + presentations, + username, + isOwnProfile, + shareMenuOpen, + presentationLinkCopied, + onShare, + onView, + onEdit, + onDelete, + menuRefs, +}: PresentationsGridProps) { + return ( +
+ {/* Header */} +
+
+ +
+
+

+ {isOwnProfile ? "My Presentations" : "Public Presentations"} +

+

+ {isOwnProfile + ? "Manage and view all your presentations" + : `View ${username}'s public presentations`} +

+
+
+ + {/* Presentations Grid */} + {presentations.length > 0 ? ( +
+ {presentations.map((pres) => ( + { + menuRefs[pres.slug] = el; + }} + /> + ))} +
+ ) : ( +
+
+ +
+

+ {isOwnProfile ? "No presentations yet" : "No public presentations"} +

+

+ {isOwnProfile + ? "Create your first presentation to get started" + : `${username} hasn't shared any public presentations yet`} +

+
+ )} +
+ ); +} diff --git a/frontend/src/features/profile/components/ProfileCard.tsx b/frontend/src/features/profile/components/ProfileCard.tsx new file mode 100644 index 0000000..05e98c5 --- /dev/null +++ b/frontend/src/features/profile/components/ProfileCard.tsx @@ -0,0 +1,252 @@ +"use client"; + +import Image from "next/image"; +import { + User, + Mail, + Calendar, + Package, + Settings, + Edit2, + Check, + X, + Loader2, +} from "lucide-react"; +import type { ProfileUser } from "../types"; +import { formatFullDate } from "../utils"; + +interface ProfileCardProps { + username: string; + isOwnProfile: boolean; + user: { + name?: string; + username?: string; + email?: string; + createdAt?: string; + } | null; + profileUser: ProfileUser | null; + gravatarUrl: string | null; + isEditingName: boolean; + editedName: string; + isSavingName: boolean; + profileError: string; + profileSuccess: string; + showUpgradeMessage: boolean; + onStartEditName: () => void; + onCancelEditName: () => void; + onSaveName: () => void; + onNameChange: (name: string) => void; + onChangePlan: () => void; + onGravatarError: () => void; +} + +export function ProfileCard({ + username, + isOwnProfile, + user, + profileUser, + gravatarUrl, + isEditingName, + editedName, + isSavingName, + profileError, + profileSuccess, + showUpgradeMessage, + onStartEditName, + onCancelEditName, + onSaveName, + onNameChange, + onChangePlan, + onGravatarError, +}: ProfileCardProps) { + return ( +
+ {/* Avatar */} +
+ {isOwnProfile && gravatarUrl ? ( +
+ {user?.name +
+ ) : ( +
+ +
+ )} +
+ + {/* Messages */} + {profileSuccess && ( +
+

+ {profileSuccess} +

+
+ )} + + {profileError && ( +
+

+ {profileError} +

+
+ )} + + {/* Name */} +
+ {isOwnProfile && isEditingName ? ( +
+ onNameChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && editedName.trim() && !isSavingName) { + onSaveName(); + } else if (e.key === "Escape") { + onCancelEditName(); + } + }} + className="text-xl font-semibold text-foreground bg-transparent border-none outline-none text-center focus:ring-0 focus:border-b-2 focus:border-primary px-1 py-0.5 min-w-0 flex-1 max-w-xs disabled:opacity-50" + placeholder="Enter your name" + disabled={isSavingName} + autoFocus + /> + + +
+ ) : ( +
+

+ {isOwnProfile + ? user?.name || user?.username + : profileUser?.name || username} +

+ {isOwnProfile && ( + + )} +
+ )} +
+ + {/* User Details */} +
+ {/* Username */} +
+ +
+

Username

+

@{username}

+
+
+ + {/* Email - only for own profile */} + {isOwnProfile && user?.email && ( +
+ +
+

Email

+

{user.email}

+
+
+ )} + + {/* Member Since */} +
+ +
+

Member Since

+

+ {formatFullDate( + isOwnProfile ? user?.createdAt : profileUser?.createdAt + )} +

+
+
+ + {/* Subscription - only for own profile */} + {isOwnProfile && ( +
+ +
+

Plan

+

Basic Plan

+
+
+ )} +
+ + {/* Account Actions - only for own profile */} + {isOwnProfile && ( +
+ +
+ )} + + {showUpgradeMessage && ( +
+

+ Currently, you cannot upgrade your plan without a referral link. +
+
+ In March 2026, the other plans will be available publicly. +
+
+ You can{" "} + + donate + {" "} + for supporting faster development. +

+
+ )} +
+ ); +} diff --git a/frontend/src/features/profile/components/ShareMenu.tsx b/frontend/src/features/profile/components/ShareMenu.tsx new file mode 100644 index 0000000..d9c781f --- /dev/null +++ b/frontend/src/features/profile/components/ShareMenu.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { Check, Copy, Share2 } from "lucide-react"; +import type { SharePlatform } from "../types"; + +interface ShareMenuProps { + isOpen: boolean; + slug: string; + name: string; + isCopied: boolean; + onShare: (slug: string, name: string, platform?: SharePlatform) => void; + menuRef: (el: HTMLDivElement | null) => void; +} + +export function ShareMenu({ + isOpen, + slug, + name, + isCopied, + onShare, + menuRef, +}: ShareMenuProps) { + if (!isOpen) return null; + + return ( +
+ {/* Copy Link */} + + + {/* Divider */} +
+ + {/* Social platforms */} +
+ + + + + +
+
+ ); +} diff --git a/frontend/src/features/profile/components/ShareProfileBox.tsx b/frontend/src/features/profile/components/ShareProfileBox.tsx new file mode 100644 index 0000000..4400e5d --- /dev/null +++ b/frontend/src/features/profile/components/ShareProfileBox.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { Share2, Copy, Check } from "lucide-react"; +import type { SharePlatform } from "../types"; + +interface ShareProfileBoxProps { + linkCopied: boolean; + onCopyLink: () => void; + onShare: (platform: Exclude) => void; +} + +export function ShareProfileBox({ + linkCopied, + onCopyLink, + onShare, +}: ShareProfileBoxProps) { + return ( +
+
+
+ +
+
+

+ Share Profile +

+

+ Share your presentations with others +

+
+
+ + {/* Copy Link Button */} + + + {/* Divider */} +
+
+ or share on +
+
+ + {/* Social Share Buttons */} +
+ + + + + +
+
+ ); +} diff --git a/frontend/src/features/profile/components/SharedPresentationsGrid.tsx b/frontend/src/features/profile/components/SharedPresentationsGrid.tsx new file mode 100644 index 0000000..1f313c4 --- /dev/null +++ b/frontend/src/features/profile/components/SharedPresentationsGrid.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { ContactRound, Users } from "lucide-react"; +import type { Presentation } from "@/features/presentation/services/presentationService"; +import type { SharePlatform } from "../types"; +import { PresentationCard } from "./PresentationCard"; + +interface SharedPresentationsGridProps { + presentations: Presentation[]; + username: string; + shareMenuOpen: string | null; + presentationLinkCopied: string | null; + onShare: (slug: string, name: string, platform?: SharePlatform) => void; + onView: (slug: string) => void; + onEdit: (presentation: Presentation) => void; + onDelete: (slug: string, name: string) => void; + menuRefs: Record; +} + +export function SharedPresentationsGrid({ + presentations, + username, + shareMenuOpen, + presentationLinkCopied, + onShare, + onView, + onEdit, + onDelete, + menuRefs, +}: SharedPresentationsGridProps) { + return ( +
+ {/* Header */} +
+
+ +
+
+

+ Shared with Me +

+

+ Presentations shared with you by other users +

+
+
+ + {/* Presentations Grid */} + {presentations.length > 0 ? ( +
+ {presentations.map((pres) => ( + { + menuRefs[pres.slug] = el; + }} + /> + ))} +
+ ) : ( +
+
+ +
+

+ No shared presentations +

+

+ Presentations shared with you will appear here +

+
+ )} +
+ ); +} diff --git a/frontend/src/features/profile/components/index.ts b/frontend/src/features/profile/components/index.ts new file mode 100644 index 0000000..84c2d24 --- /dev/null +++ b/frontend/src/features/profile/components/index.ts @@ -0,0 +1,6 @@ +export { ProfileCard } from "./ProfileCard"; +export { ShareProfileBox } from "./ShareProfileBox"; +export { PresentationsGrid } from "./PresentationsGrid"; +export { SharedPresentationsGrid } from "./SharedPresentationsGrid"; +export { PresentationCard } from "./PresentationCard"; +export { ShareMenu } from "./ShareMenu"; diff --git a/frontend/src/features/profile/index.ts b/frontend/src/features/profile/index.ts new file mode 100644 index 0000000..b793f51 --- /dev/null +++ b/frontend/src/features/profile/index.ts @@ -0,0 +1,3 @@ +export * from "./components"; +export * from "./types"; +export * from "./utils"; diff --git a/frontend/src/features/profile/types/index.ts b/frontend/src/features/profile/types/index.ts new file mode 100644 index 0000000..7def5cd --- /dev/null +++ b/frontend/src/features/profile/types/index.ts @@ -0,0 +1 @@ +export * from "./profile.types"; diff --git a/frontend/src/features/profile/types/profile.types.ts b/frontend/src/features/profile/types/profile.types.ts new file mode 100644 index 0000000..f763c53 --- /dev/null +++ b/frontend/src/features/profile/types/profile.types.ts @@ -0,0 +1,20 @@ +import type { Presentation } from "@/features/presentation/services/presentationService"; + +export interface ProfileUser { + name?: string; + username: string; + createdAt?: string; +} + +export interface DeleteModalState { + isOpen: boolean; + slug: string; + name: string; +} + +export interface EditModalState { + isOpen: boolean; + presentation: Presentation | null; +} + +export type SharePlatform = "twitter" | "facebook" | "linkedin" | "copy"; diff --git a/frontend/src/features/profile/utils/index.ts b/frontend/src/features/profile/utils/index.ts new file mode 100644 index 0000000..ae7d5ab --- /dev/null +++ b/frontend/src/features/profile/utils/index.ts @@ -0,0 +1 @@ +export * from "./profile.utils"; diff --git a/frontend/src/features/profile/utils/profile.utils.ts b/frontend/src/features/profile/utils/profile.utils.ts new file mode 100644 index 0000000..a82fecd --- /dev/null +++ b/frontend/src/features/profile/utils/profile.utils.ts @@ -0,0 +1,45 @@ +import CryptoJS from "crypto-js"; + +export const GRAVATAR_DEFAULT_SIZE = 96; +export const COPY_FEEDBACK_DURATION = 2000; + +/** + * Generate Gravatar URL from email + */ +export function getGravatarUrl( + email: string, + size: number = GRAVATAR_DEFAULT_SIZE +): string { + const normalizedEmail = email.toLowerCase().trim(); + const emailHash = CryptoJS.MD5(normalizedEmail).toString(); + return `https://www.gravatar.com/avatar/${emailHash}?s=${size}&d=identicon&r=pg`; +} + +/** + * Format date to short format (e.g., "Jan 15, 2024") + */ +export function formatDate(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +/** + * Format date to full format (e.g., "January 15, 2024") + */ +export function formatFullDate(dateString?: string): string { + if (!dateString) return "N/A"; + try { + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + } catch { + return "N/A"; + } +} From f5d426026d52cd4cbfb9da09a3ac4ef2eaed8526 Mon Sep 17 00:00:00 2001 From: Mostafa Mirmousavi Date: Tue, 2 Dec 2025 07:44:22 +0100 Subject: [PATCH 8/9] feat: add template --- frontend/src/app/[username]/page.tsx | 43 +++++++--- .../components/SharedPresentationsGrid.tsx | 4 +- .../profile/components/TemplatesGrid.tsx | 82 +++++++++++++++++++ .../src/features/profile/components/index.ts | 1 + 4 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 frontend/src/features/profile/components/TemplatesGrid.tsx diff --git a/frontend/src/app/[username]/page.tsx b/frontend/src/app/[username]/page.tsx index c9dba55..ca38e87 100644 --- a/frontend/src/app/[username]/page.tsx +++ b/frontend/src/app/[username]/page.tsx @@ -24,6 +24,7 @@ import { ShareProfileBox, PresentationsGrid, SharedPresentationsGrid, + TemplatesGrid, } from "@/features/profile"; import type { ProfileUser, @@ -65,6 +66,11 @@ export default function UserProfilePage() { Presentation[] >([]); + // Templates + // Note: setTemplates will be used when backend API is implemented + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [templates, setTemplates] = useState([]); + // Presentation modals const [deleteModal, setDeleteModal] = useState({ isOpen: false, @@ -510,17 +516,32 @@ export default function UserProfilePage() { {/* Shared Presentations - only for own profile */} {isOwnProfile && ( - + <> + + + {/* Templates */} + + )}

diff --git a/frontend/src/features/profile/components/SharedPresentationsGrid.tsx b/frontend/src/features/profile/components/SharedPresentationsGrid.tsx index 1f313c4..63e05b2 100644 --- a/frontend/src/features/profile/components/SharedPresentationsGrid.tsx +++ b/frontend/src/features/profile/components/SharedPresentationsGrid.tsx @@ -1,6 +1,6 @@ "use client"; -import { ContactRound, Users } from "lucide-react"; +import { FileUser, Users } from "lucide-react"; import type { Presentation } from "@/features/presentation/services/presentationService"; import type { SharePlatform } from "../types"; import { PresentationCard } from "./PresentationCard"; @@ -69,7 +69,7 @@ export function SharedPresentationsGrid({ ) : (
- +

No shared presentations diff --git a/frontend/src/features/profile/components/TemplatesGrid.tsx b/frontend/src/features/profile/components/TemplatesGrid.tsx new file mode 100644 index 0000000..09f4d54 --- /dev/null +++ b/frontend/src/features/profile/components/TemplatesGrid.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { FileSymlink } from "lucide-react"; +import type { Presentation } from "@/features/presentation/services/presentationService"; +import type { SharePlatform } from "../types"; +import { PresentationCard } from "./PresentationCard"; + +interface TemplatesGridProps { + templates: Presentation[]; + username: string; + shareMenuOpen: string | null; + presentationLinkCopied: string | null; + onShare: (slug: string, name: string, platform?: SharePlatform) => void; + onView: (slug: string) => void; + onEdit: (presentation: Presentation) => void; + onDelete: (slug: string, name: string) => void; + menuRefs: Record; +} + +export function TemplatesGrid({ + templates, + username, + shareMenuOpen, + presentationLinkCopied, + onShare, + onView, + onEdit, + onDelete, + menuRefs, +}: TemplatesGridProps) { + return ( +
+ {/* Header */} +
+
+ +
+
+

Templates

+

+ Browse and use presentation templates +

+
+
+ + {/* Templates Grid */} + {templates.length > 0 ? ( +
+ {templates.map((template) => ( + { + menuRefs[template.slug] = el; + }} + /> + ))} +
+ ) : ( +
+
+ +
+

+ No templates available +

+

+ Templates will appear here when they become available +

+
+ )} +
+ ); +} diff --git a/frontend/src/features/profile/components/index.ts b/frontend/src/features/profile/components/index.ts index 84c2d24..cdad644 100644 --- a/frontend/src/features/profile/components/index.ts +++ b/frontend/src/features/profile/components/index.ts @@ -2,5 +2,6 @@ export { ProfileCard } from "./ProfileCard"; export { ShareProfileBox } from "./ShareProfileBox"; export { PresentationsGrid } from "./PresentationsGrid"; export { SharedPresentationsGrid } from "./SharedPresentationsGrid"; +export { TemplatesGrid } from "./TemplatesGrid"; export { PresentationCard } from "./PresentationCard"; export { ShareMenu } from "./ShareMenu"; From e45c135f12672a461b60feffd119cebefbb1fe7e Mon Sep 17 00:00:00 2001 From: Mostafa Mirmousavi Date: Tue, 2 Dec 2025 07:53:09 +0100 Subject: [PATCH 9/9] fix: remove shared presentation --- frontend/src/app/[username]/page.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/frontend/src/app/[username]/page.tsx b/frontend/src/app/[username]/page.tsx index e425143..ca38e87 100644 --- a/frontend/src/app/[username]/page.tsx +++ b/frontend/src/app/[username]/page.tsx @@ -542,17 +542,6 @@ export default function UserProfilePage() { menuRefs={shareMenuRefs.current} /> - )}