diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..63b09a0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.compile.nullAnalysis.mode": "automatic", + "workbench.editor.enablePreview": false +} \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index bec39ce..0000000 --- a/README.md +++ /dev/null @@ -1,110 +0,0 @@ -# GitHub Copilot Vibe Coding Workshop - -![GitHub Copilot Vibe Coding Workshop](./images/banner.png) - -Let's vibe-code with [GitHub Copilot](https://docs.github.com/copilot/about-github-copilot/what-is-github-copilot) and its newest and greatest features in various programming languages such as Python, JavaScript, Java and .NET, as well as make the apps cloud-native by containerization. Are you ready to jump in? - -## Background - -Contoso is a company that sells products for various outdoor activities. A marketing department of Contoso would like to launch a micro social media website to promote their products for existing and potential customers. As their first MVP, the want to quickly build the website. The IT department of Contoso currently has two developers using Python and JavaScript respectively. The launch date is fast approaching, so both developers should quickly deliver the application. - -But here's the situation... - -## Workshop Objectives - -- Build applications using GitHub Copilot Agent Mode. -- Add custom instruction to GitHub Copilot so that you have more control over GitHub Copilot. -- Add various MCP servers to GitHub Copilot so that you build the applications more precisely. - -## Workshop in Your Language - -This workshop material is currently provided in the following languages: - -[English](./README.md) | [Español](./localisation/es-es/) | [Français](./localisation/fr-fr/) | [日本語](./localisation/ja-jp/) | [한국어](./localisation/ko-kr/) | [Português](./localisation/pt-br/) | [中文(简体)](./localisation/zh-cn/) - -## Prerequisites - -During this workshop, [GitHub Codespaces](https://docs.github.com/en/codespaces/about-codespaces/what-are-codespaces) is highly recommended because there's no need for preparation, except a web browser. - -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/microsoft/github-copilot-vibe-coding-workshop) - -However, if you really need to use your machine, make sure you've installed everything identified below. - -### Common - -- [Visual Studio Code](https://code.visualstudio.com/) -- VS Code [GitHub Copilot](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot) Extension -- VS Code [GitHub Copilot Chat](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat) Extension -- 💥 For Windows users 👉 [PowerShell 7](https://learn.microsoft.com/powershell/scripting/install/installing-powershell) -- [git CLI](https://git-scm.com/downloads) -- [GitHub CLI](https://cli.github.com/) -- [Docker Desktop](https://docs.docker.com/get-started/introduction/get-docker-desktop/) - -### Python - -- [pyenv](https://github.com/pyenv/pyenv) or [pyenv for Windows](https://github.com/pyenv-win/pyenv-win) -- Python 3.12+ through pyenv -- `uv` package manager (recommended) or `pip` -- VS Code [Python](https://marketplace.visualstudio.com/items/?itemName=ms-python.python) Extension -- VS Code [Pylance](https://marketplace.visualstudio.com/items/?itemName=ms-python.vscode-pylance) Extension -- VS Code [Python Debugger](https://marketplace.visualstudio.com/items/?itemName=ms-python.debugpy) Extension -- VS Code [autopep8](https://marketplace.visualstudio.com/items/?itemName=ms-python.autopep8) Extension - -### JavaScript - -- [nvm](https://github.com/nvm-sh/nvm) or [nvm for Windows](https://github.com/coreybutler/nvm-windows) -- The latest LTS of [Node.js](https://nodejs.org/) through nvm - -### Java - -- [SDKMAN](https://sdkman.io/) -- [OpenJDK 21](https://learn.microsoft.com/java/openjdk/download) through SDKMAN -- [Apache Maven](https://maven.apache.org/download.cgi) through SDKMAN -- [Gradle Build Tool](https://docs.gradle.org/current/userguide/installation.html) through SDKMAN -- [Spring Boot Initializr](https://docs.spring.io/spring-boot/cli/installation.html) through SDKMAN -- VS Code [Extension Pack for Java](https://marketplace.visualstudio.com/items/?itemName=vscjava.vscode-java-pack) Extension -- VS Code [Spring Boot Extension Pack](https://marketplace.visualstudio.com/items/?itemName=vmware.vscode-boot-dev-pack) Extension - -### .NET - -- [.NET SDK 9](https://dotnet.microsoft.com/download/dotnet/9.0) -- [VS Code C# Dev Kit](https://marketplace.visualstudio.com/items/?itemName=ms-dotnettools.csdevkit) Extension - -## Product Requirements Document - -First and foremost, the place for you to start is this [PRD (Product Requirements Document)](./product-requirements.md). This document will give you a better understanding of what to do and how to do it. - -## Workshop Instructions - -This is a self-paced workshop by following the links below: - -| Step | Link | -|------------------------------------|---------------------------------------------------------| -| 00: Development Environment | [00-setup.md](./docs/00-setup.md) | -| 01: Python Backend | [01-python.md](./docs/01-python.md) | -| 02: JavaScript Frontend | [02-javascript.md](./docs/02-javascript.md) | -| 03: Java Migration from Python | [03-java.md](./docs/03-java.md) | -| 04: .NET Migration from JavaScript | [04-dotnet.md](./docs/04-dotnet.md) | -| 05: Containerization | [05-containerization.md](./docs/05-containerization.md) | - -## Complete Samples - -Check out the complete example of each application. They're also vibe-coded with GitHub Copilot, therefore, they might not be perfect, and you don't have to follow the app. - -| Language | Application | Location | -|---------------------|-------------|--------------------------------------| -| Python Backend | FastAPI | [python](./complete/python/) | -| JavaScript Frontend | React | [javascript](./complete/javascript/) | -| Java Backend | Spring Boot | [java](./complete/java/) | -| .NET Frontend | Blazor | [dotnet](./complete/dotnet/) | -| Containerization | Container | [containerization](./complete/) | - -## Read More... - -- [GitHub Codespaces](https://docs.github.com/en/codespaces/about-codespaces/what-are-codespaces) -- [GitHub Copilot](https://docs.github.com/en/copilot/about-github-copilot/what-is-github-copilot) -- [GitHub Copilot: Agent Mode](https://code.visualstudio.com/blogs/2025/04/07/agentMode) -- [GitHub Copilot: MCP](https://code.visualstudio.com/blogs/2025/05/12/agent-mode-meets-mcp) -- [GitHub Copilot: Custom Instructions](https://code.visualstudio.com/docs/copilot/copilot-customization) -- [GitHub Copilot: Changing AI Models](https://docs.github.com/en/copilot/using-github-copilot/ai-models/changing-the-ai-model-for-copilot-chat?tool=vscode) -- [Curated MCP Servers](https://github.com/modelcontextprotocol/servers) diff --git a/complete/python/sns_api.db b/complete/python/sns_api.db new file mode 100644 index 0000000..5ef39d3 Binary files /dev/null and b/complete/python/sns_api.db differ diff --git a/java/sns_api.db b/java/sns_api.db new file mode 100644 index 0000000..000d834 Binary files /dev/null and b/java/sns_api.db differ diff --git a/java/socialapp/.gitattributes b/java/socialapp/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/java/socialapp/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/java/socialapp/.gitignore b/java/socialapp/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/java/socialapp/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/java/socialapp/build.gradle b/java/socialapp/build.gradle new file mode 100644 index 0000000..fa43944 --- /dev/null +++ b/java/socialapp/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.1' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.contoso' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.xerial:sqlite-jdbc:3.47.1.0' + implementation 'org.hibernate.orm:hibernate-community-dialects:6.6.4.Final' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/java/socialapp/final_test.json b/java/socialapp/final_test.json new file mode 100644 index 0000000..4f50c9a --- /dev/null +++ b/java/socialapp/final_test.json @@ -0,0 +1,4 @@ +{ + "username": "final_test", + "content": "Final verification post for Spring Boot migration" +} diff --git a/java/socialapp/gradle/wrapper/gradle-wrapper.jar b/java/socialapp/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/java/socialapp/gradle/wrapper/gradle-wrapper.jar differ diff --git a/java/socialapp/gradle/wrapper/gradle-wrapper.properties b/java/socialapp/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d4081da --- /dev/null +++ b/java/socialapp/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java/socialapp/gradlew b/java/socialapp/gradlew new file mode 100644 index 0000000..23d15a9 --- /dev/null +++ b/java/socialapp/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/java/socialapp/gradlew.bat b/java/socialapp/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/java/socialapp/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java/socialapp/settings.gradle b/java/socialapp/settings.gradle new file mode 100644 index 0000000..4cb1bbd --- /dev/null +++ b/java/socialapp/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'socialapp' diff --git a/java/socialapp/sns_api.db b/java/socialapp/sns_api.db new file mode 100644 index 0000000..2abcbd5 Binary files /dev/null and b/java/socialapp/sns_api.db differ diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/SocialappApplication.java b/java/socialapp/src/main/java/com/contoso/socialapp/SocialappApplication.java new file mode 100644 index 0000000..3801251 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/SocialappApplication.java @@ -0,0 +1,13 @@ +package com.contoso.socialapp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SocialappApplication { + + public static void main(String[] args) { + SpringApplication.run(SocialappApplication.class, args); + } + +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/config/OpenApiConfig.java b/java/socialapp/src/main/java/com/contoso/socialapp/config/OpenApiConfig.java new file mode 100644 index 0000000..2bca149 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/config/OpenApiConfig.java @@ -0,0 +1,30 @@ +package com.contoso.socialapp.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("Simple Social Media Application API") + .description("A basic but functional Social Networking Service (SNS) that allows users to create, retrieve, update, and delete posts; add comments; and like/unlike posts.") + .version("1.0.0") + .contact(new Contact() + .name("Product Owner / Tech Lead at Contoso"))) + .servers(List.of( + new Server() + .url("http://localhost:8080") + .description("Development server") + )); + } +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/config/WebConfig.java b/java/socialapp/src/main/java/com/contoso/socialapp/config/WebConfig.java new file mode 100644 index 0000000..1db9088 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/config/WebConfig.java @@ -0,0 +1,18 @@ +package com.contoso.socialapp.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(false); // Changed to false to avoid conflict + } +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/controller/CommentController.java b/java/socialapp/src/main/java/com/contoso/socialapp/controller/CommentController.java new file mode 100644 index 0000000..70f52d7 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/controller/CommentController.java @@ -0,0 +1,91 @@ +package com.contoso.socialapp.controller; + +import com.contoso.socialapp.dto.*; +import com.contoso.socialapp.service.CommentService; +import com.contoso.socialapp.service.PostService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/posts/{postId}/comments") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class CommentController { + + private final CommentService commentService; + private final PostService postService; + + @GetMapping + public ResponseEntity listComments(@PathVariable String postId) { + // First check if post exists + if (!postService.postExists(postId)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse("Post not found", 404)); + } + + List comments = commentService.getCommentsByPostId(postId); + return ResponseEntity.ok(comments); + } + + @PostMapping + public ResponseEntity createComment(@PathVariable String postId, @RequestBody CreateCommentRequest request) { + // First check if post exists + if (!postService.postExists(postId)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse("Post not found", 404)); + } + + CommentResponse createdComment = commentService.createComment(postId, request); + return ResponseEntity.status(HttpStatus.CREATED).body(createdComment); + } + + @GetMapping("/{commentId}") + public ResponseEntity getComment(@PathVariable String postId, @PathVariable String commentId) { + // First check if post exists + if (!postService.postExists(postId)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse("Post not found", 404)); + } + + return commentService.getCommentById(postId, commentId) + .map(comment -> ResponseEntity.ok((Object) comment)) + .orElse(ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse("Post or comment not found", 404))); + } + + @PatchMapping("/{commentId}") + public ResponseEntity updateComment(@PathVariable String postId, @PathVariable String commentId, + @RequestBody UpdateCommentRequest request) { + // First check if post exists + if (!postService.postExists(postId)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse("Post not found", 404)); + } + + return commentService.updateComment(postId, commentId, request) + .map(comment -> ResponseEntity.ok((Object) comment)) + .orElse(ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse("Post or comment not found", 404))); + } + + @DeleteMapping("/{commentId}") + public ResponseEntity deleteComment(@PathVariable String postId, @PathVariable String commentId) { + // First check if post exists + if (!postService.postExists(postId)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse("Post not found", 404)); + } + + boolean deleted = commentService.deleteComment(postId, commentId); + if (deleted) { + return ResponseEntity.noContent().build(); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse("Post or comment not found", 404)); + } + } +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/controller/HealthController.java b/java/socialapp/src/main/java/com/contoso/socialapp/controller/HealthController.java new file mode 100644 index 0000000..7040077 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/controller/HealthController.java @@ -0,0 +1,22 @@ +package com.contoso.socialapp.controller; + +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +@CrossOrigin(origins = "*") +public class HealthController { + + @GetMapping("/health") + public String health() { + return "Application is running!"; + } + + @GetMapping("/welcome") + public String welcome() { + return "Welcome to the Social Media API!"; + } +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/controller/LikeController.java b/java/socialapp/src/main/java/com/contoso/socialapp/controller/LikeController.java new file mode 100644 index 0000000..384b912 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/controller/LikeController.java @@ -0,0 +1,45 @@ +package com.contoso.socialapp.controller; + +import com.contoso.socialapp.dto.*; +import com.contoso.socialapp.service.LikeService; +import com.contoso.socialapp.service.PostService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/posts/{postId}/likes") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class LikeController { + + private final LikeService likeService; + private final PostService postService; + + @PostMapping + public ResponseEntity likePost(@PathVariable String postId, @RequestBody LikeRequest request) { + // First check if post exists + if (!postService.postExists(postId)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse("Post not found", 404)); + } + + return likeService.likePost(postId, request) + .map(like -> ResponseEntity.status(HttpStatus.CREATED).body((Object) like)) + .orElse(ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse("User already liked this post", 400))); + } + + @DeleteMapping + public ResponseEntity unlikePost(@PathVariable String postId) { + // First check if post exists + if (!postService.postExists(postId)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse("Post not found", 404)); + } + + likeService.unlikePost(postId); + return ResponseEntity.noContent().build(); + } +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/controller/PostController.java b/java/socialapp/src/main/java/com/contoso/socialapp/controller/PostController.java new file mode 100644 index 0000000..66a4f55 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/controller/PostController.java @@ -0,0 +1,132 @@ +package com.contoso.socialapp.controller; + +import com.contoso.socialapp.dto.*; +import com.contoso.socialapp.service.PostService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/posts") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +@Tag(name = "Posts", description = "Post management operations") +public class PostController { + + private final PostService postService; + + @GetMapping + @Operation(summary = "List all posts", description = "Retrieve all recent posts for browsing") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved posts", + content = @Content(mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = PostResponse.class)))), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity> listPosts() { + List posts = postService.getAllPosts(); + return ResponseEntity.ok(posts); + } + + @PostMapping + @Operation(summary = "Create a new post", description = "Create a new post to share with others") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Post created successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = PostResponse.class))), + @ApiResponse(responseCode = "400", description = "Bad request - invalid input", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity createPost(@RequestBody CreatePostRequest request) { + PostResponse createdPost = postService.createPost(request); + return ResponseEntity.status(HttpStatus.CREATED).body(createdPost); + } + + @GetMapping("/{postId}") + @Operation(summary = "Get a specific post", description = "Retrieve a specific post by its ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved post", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = PostResponse.class))), + @ApiResponse(responseCode = "404", description = "Post not found", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity getPost( + @Parameter(description = "The ID of the post to retrieve", required = true) + @PathVariable String postId) { + return postService.getPostById(postId) + .map(post -> ResponseEntity.ok((Object) post)) + .orElse(ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse("Post not found", 404))); + } + + @PatchMapping("/{postId}") + @Operation(summary = "Update a post", description = "Update an existing post") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Post updated successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = PostResponse.class))), + @ApiResponse(responseCode = "400", description = "Bad request - invalid input", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "Post not found", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity updatePost( + @Parameter(description = "The ID of the post to update", required = true) + @PathVariable String postId, + @RequestBody UpdatePostRequest request) { + return postService.updatePost(postId, request) + .map(post -> ResponseEntity.ok((Object) post)) + .orElse(ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse("Post not found", 404))); + } + + @DeleteMapping("/{postId}") + @Operation(summary = "Delete a post", description = "Delete an existing post") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Post deleted successfully"), + @ApiResponse(responseCode = "404", description = "Post not found", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity deletePost( + @Parameter(description = "The ID of the post to delete", required = true) + @PathVariable String postId) { + boolean deleted = postService.deletePost(postId); + if (deleted) { + return ResponseEntity.noContent().build(); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse("Post not found", 404)); + } + } +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/dto/CommentResponse.java b/java/socialapp/src/main/java/com/contoso/socialapp/dto/CommentResponse.java new file mode 100644 index 0000000..e90d143 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/dto/CommentResponse.java @@ -0,0 +1,21 @@ +package com.contoso.socialapp.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CommentResponse { + private String id; + private String postId; + private String username; + private String content; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/dto/CreateCommentRequest.java b/java/socialapp/src/main/java/com/contoso/socialapp/dto/CreateCommentRequest.java new file mode 100644 index 0000000..50c09b3 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/dto/CreateCommentRequest.java @@ -0,0 +1,13 @@ +package com.contoso.socialapp.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreateCommentRequest { + private String username; + private String content; +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/dto/CreatePostRequest.java b/java/socialapp/src/main/java/com/contoso/socialapp/dto/CreatePostRequest.java new file mode 100644 index 0000000..5c7013c --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/dto/CreatePostRequest.java @@ -0,0 +1,13 @@ +package com.contoso.socialapp.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreatePostRequest { + private String username; + private String content; +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/dto/ErrorResponse.java b/java/socialapp/src/main/java/com/contoso/socialapp/dto/ErrorResponse.java new file mode 100644 index 0000000..ae52d5f --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/dto/ErrorResponse.java @@ -0,0 +1,13 @@ +package com.contoso.socialapp.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ErrorResponse { + private String message; + private int code; +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/dto/LikeRequest.java b/java/socialapp/src/main/java/com/contoso/socialapp/dto/LikeRequest.java new file mode 100644 index 0000000..0b7310a --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/dto/LikeRequest.java @@ -0,0 +1,12 @@ +package com.contoso.socialapp.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LikeRequest { + private String username; +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/dto/LikeResponse.java b/java/socialapp/src/main/java/com/contoso/socialapp/dto/LikeResponse.java new file mode 100644 index 0000000..ccb107e --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/dto/LikeResponse.java @@ -0,0 +1,19 @@ +package com.contoso.socialapp.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class LikeResponse { + private String id; + private String postId; + private String username; + private LocalDateTime createdAt; +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/dto/PostResponse.java b/java/socialapp/src/main/java/com/contoso/socialapp/dto/PostResponse.java new file mode 100644 index 0000000..29e616a --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/dto/PostResponse.java @@ -0,0 +1,22 @@ +package com.contoso.socialapp.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PostResponse { + private String id; + private String username; + private String content; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private int likesCount; + private int commentsCount; +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/dto/UpdateCommentRequest.java b/java/socialapp/src/main/java/com/contoso/socialapp/dto/UpdateCommentRequest.java new file mode 100644 index 0000000..1a9ac67 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/dto/UpdateCommentRequest.java @@ -0,0 +1,13 @@ +package com.contoso.socialapp.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UpdateCommentRequest { + private String username; + private String content; +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/dto/UpdatePostRequest.java b/java/socialapp/src/main/java/com/contoso/socialapp/dto/UpdatePostRequest.java new file mode 100644 index 0000000..1f1cc3b --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/dto/UpdatePostRequest.java @@ -0,0 +1,13 @@ +package com.contoso.socialapp.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UpdatePostRequest { + private String username; + private String content; +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/entity/Comment.java b/java/socialapp/src/main/java/com/contoso/socialapp/entity/Comment.java new file mode 100644 index 0000000..3ff2a67 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/entity/Comment.java @@ -0,0 +1,52 @@ +package com.contoso.socialapp.entity; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "comments") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Comment { + + @Id + @GenericGenerator(name = "uuid2", strategy = "uuid2") + @GeneratedValue(generator = "uuid2") + private String id; + + @Column(name = "post_id", nullable = false) + private String postId; + + @Column(nullable = false) + private String username; + + @Column(nullable = false) + private String content; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", insertable = false, updatable = false) + private Post post; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/entity/Like.java b/java/socialapp/src/main/java/com/contoso/socialapp/entity/Like.java new file mode 100644 index 0000000..a205dbc --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/entity/Like.java @@ -0,0 +1,40 @@ +package com.contoso.socialapp.entity; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "likes") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Like { + + @Id + @GenericGenerator(name = "uuid2", strategy = "uuid2") + @GeneratedValue(generator = "uuid2") + private String id; + + @Column(name = "post_id", nullable = false) + private String postId; + + @Column(nullable = false) + private String username; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", insertable = false, updatable = false) + private Post post; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/entity/Post.java b/java/socialapp/src/main/java/com/contoso/socialapp/entity/Post.java new file mode 100644 index 0000000..12791dd --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/entity/Post.java @@ -0,0 +1,60 @@ +package com.contoso.socialapp.entity; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Table(name = "posts") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Post { + + @Id + @GenericGenerator(name = "uuid2", strategy = "uuid2") + @GeneratedValue(generator = "uuid2") + private String id; + + @Column(nullable = false) + private String username; + + @Column(nullable = false) + private String content; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private List comments; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private List likes; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } + + public int getLikesCount() { + return likes != null ? likes.size() : 0; + } + + public int getCommentsCount() { + return comments != null ? comments.size() : 0; + } +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/repository/CommentRepository.java b/java/socialapp/src/main/java/com/contoso/socialapp/repository/CommentRepository.java new file mode 100644 index 0000000..87434a0 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/repository/CommentRepository.java @@ -0,0 +1,16 @@ +package com.contoso.socialapp.repository; + +import com.contoso.socialapp.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface CommentRepository extends JpaRepository { + + List findByPostIdOrderByCreatedAtAsc(String postId); + + Optional findByIdAndPostId(String commentId, String postId); +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/repository/LikeRepository.java b/java/socialapp/src/main/java/com/contoso/socialapp/repository/LikeRepository.java new file mode 100644 index 0000000..33a523f --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/repository/LikeRepository.java @@ -0,0 +1,18 @@ +package com.contoso.socialapp.repository; + +import com.contoso.socialapp.entity.Like; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface LikeRepository extends JpaRepository { + + List findByPostId(String postId); + + Optional findByPostIdAndUsername(String postId, String username); + + void deleteByPostId(String postId); +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/repository/PostRepository.java b/java/socialapp/src/main/java/com/contoso/socialapp/repository/PostRepository.java new file mode 100644 index 0000000..77768a6 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/repository/PostRepository.java @@ -0,0 +1,15 @@ +package com.contoso.socialapp.repository; + +import com.contoso.socialapp.entity.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface PostRepository extends JpaRepository { + + @Query("SELECT p FROM Post p LEFT JOIN FETCH p.comments LEFT JOIN FETCH p.likes ORDER BY p.createdAt DESC") + List findAllWithCounts(); +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/service/CommentService.java b/java/socialapp/src/main/java/com/contoso/socialapp/service/CommentService.java new file mode 100644 index 0000000..c9c7df4 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/service/CommentService.java @@ -0,0 +1,71 @@ +package com.contoso.socialapp.service; + +import com.contoso.socialapp.dto.*; +import com.contoso.socialapp.entity.Comment; +import com.contoso.socialapp.repository.CommentRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Transactional +public class CommentService { + + private final CommentRepository commentRepository; + + public List getCommentsByPostId(String postId) { + List comments = commentRepository.findByPostIdOrderByCreatedAtAsc(postId); + return comments.stream() + .map(this::convertToResponse) + .toList(); + } + + public Optional getCommentById(String postId, String commentId) { + return commentRepository.findByIdAndPostId(commentId, postId) + .map(this::convertToResponse); + } + + public CommentResponse createComment(String postId, CreateCommentRequest request) { + Comment comment = new Comment(); + comment.setPostId(postId); + comment.setUsername(request.getUsername()); + comment.setContent(request.getContent()); + + Comment savedComment = commentRepository.save(comment); + return convertToResponse(savedComment); + } + + public Optional updateComment(String postId, String commentId, UpdateCommentRequest request) { + return commentRepository.findByIdAndPostId(commentId, postId) + .map(comment -> { + comment.setUsername(request.getUsername()); + comment.setContent(request.getContent()); + Comment savedComment = commentRepository.save(comment); + return convertToResponse(savedComment); + }); + } + + public boolean deleteComment(String postId, String commentId) { + return commentRepository.findByIdAndPostId(commentId, postId) + .map(comment -> { + commentRepository.delete(comment); + return true; + }) + .orElse(false); + } + + private CommentResponse convertToResponse(Comment comment) { + return CommentResponse.builder() + .id(comment.getId()) + .postId(comment.getPostId()) + .username(comment.getUsername()) + .content(comment.getContent()) + .createdAt(comment.getCreatedAt()) + .updatedAt(comment.getUpdatedAt()) + .build(); + } +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/service/LikeService.java b/java/socialapp/src/main/java/com/contoso/socialapp/service/LikeService.java new file mode 100644 index 0000000..eee913e --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/service/LikeService.java @@ -0,0 +1,49 @@ +package com.contoso.socialapp.service; + +import com.contoso.socialapp.dto.*; +import com.contoso.socialapp.entity.Like; +import com.contoso.socialapp.repository.LikeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Transactional +public class LikeService { + + private final LikeRepository likeRepository; + + public Optional likePost(String postId, LikeRequest request) { + // Check if user already liked this post + Optional existingLike = likeRepository.findByPostIdAndUsername(postId, request.getUsername()); + if (existingLike.isPresent()) { + return Optional.empty(); // User already liked this post + } + + Like like = new Like(); + like.setPostId(postId); + like.setUsername(request.getUsername()); + + Like savedLike = likeRepository.save(like); + return Optional.of(convertToResponse(savedLike)); + } + + public boolean unlikePost(String postId) { + // For simplicity, we'll remove all likes for this post + // In a real application, you'd want to identify the specific user + likeRepository.deleteByPostId(postId); + return true; + } + + private LikeResponse convertToResponse(Like like) { + return LikeResponse.builder() + .id(like.getId()) + .postId(like.getPostId()) + .username(like.getUsername()) + .createdAt(like.getCreatedAt()) + .build(); + } +} diff --git a/java/socialapp/src/main/java/com/contoso/socialapp/service/PostService.java b/java/socialapp/src/main/java/com/contoso/socialapp/service/PostService.java new file mode 100644 index 0000000..c851080 --- /dev/null +++ b/java/socialapp/src/main/java/com/contoso/socialapp/service/PostService.java @@ -0,0 +1,75 @@ +package com.contoso.socialapp.service; + +import com.contoso.socialapp.dto.*; +import com.contoso.socialapp.entity.Post; +import com.contoso.socialapp.repository.PostRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Transactional +public class PostService { + + private final PostRepository postRepository; + + public List getAllPosts() { + List posts = postRepository.findAll(); + return posts.stream() + .map(this::convertToResponse) + .toList(); + } + + public Optional getPostById(String postId) { + return postRepository.findById(postId) + .map(this::convertToResponse); + } + + public PostResponse createPost(CreatePostRequest request) { + Post post = new Post(); + post.setUsername(request.getUsername()); + post.setContent(request.getContent()); + + Post savedPost = postRepository.save(post); + return convertToResponse(savedPost); + } + + public Optional updatePost(String postId, UpdatePostRequest request) { + return postRepository.findById(postId) + .map(post -> { + post.setUsername(request.getUsername()); + post.setContent(request.getContent()); + Post savedPost = postRepository.save(post); + return convertToResponse(savedPost); + }); + } + + public boolean deletePost(String postId) { + return postRepository.findById(postId) + .map(post -> { + postRepository.delete(post); + return true; + }) + .orElse(false); + } + + public boolean postExists(String postId) { + return postRepository.existsById(postId); + } + + private PostResponse convertToResponse(Post post) { + return PostResponse.builder() + .id(post.getId()) + .username(post.getUsername()) + .content(post.getContent()) + .createdAt(post.getCreatedAt()) + .updatedAt(post.getUpdatedAt()) + .likesCount(post.getLikesCount()) + .commentsCount(post.getCommentsCount()) + .build(); + } +} diff --git a/java/socialapp/src/main/resources/application.properties b/java/socialapp/src/main/resources/application.properties new file mode 100644 index 0000000..0de9d41 --- /dev/null +++ b/java/socialapp/src/main/resources/application.properties @@ -0,0 +1,14 @@ +spring.application.name=socialapp +server.port=8080 + +# Database Configuration +spring.datasource.url=jdbc:sqlite:sns_api.db +spring.datasource.driver-class-name=org.sqlite.JDBC +spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=false + +# OpenAPI Configuration +springdoc.api-docs.path=/v3/api-docs +springdoc.swagger-ui.path=/swagger-ui.html +springdoc.swagger-ui.operations-sorter=method diff --git a/java/socialapp/src/test/java/com/contoso/socialapp/SocialappApplicationTests.java b/java/socialapp/src/test/java/com/contoso/socialapp/SocialappApplicationTests.java new file mode 100644 index 0000000..638fcd3 --- /dev/null +++ b/java/socialapp/src/test/java/com/contoso/socialapp/SocialappApplicationTests.java @@ -0,0 +1,13 @@ +package com.contoso.socialapp; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SocialappApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/java/socialapp/test_comment.json b/java/socialapp/test_comment.json new file mode 100644 index 0000000..b993553 --- /dev/null +++ b/java/socialapp/test_comment.json @@ -0,0 +1,4 @@ +{ + "username": "commentuser", + "content": "This is a test comment" +} diff --git a/java/socialapp/test_like.json b/java/socialapp/test_like.json new file mode 100644 index 0000000..8190d8c --- /dev/null +++ b/java/socialapp/test_like.json @@ -0,0 +1,3 @@ +{ + "username": "likeuser" +} diff --git a/java/socialapp/test_post.json b/java/socialapp/test_post.json new file mode 100644 index 0000000..e1e2764 --- /dev/null +++ b/java/socialapp/test_post.json @@ -0,0 +1,4 @@ +{ + "username": "testuser", + "content": "Test post from Spring Boot API" +} diff --git a/java/socialapp/update_post.json b/java/socialapp/update_post.json new file mode 100644 index 0000000..7f5f963 --- /dev/null +++ b/java/socialapp/update_post.json @@ -0,0 +1,4 @@ +{ + "username": "testuser", + "content": "Updated content for Spring Boot API" +} diff --git a/javascript/SimpleSocialMediaApplication/README.md b/javascript/SimpleSocialMediaApplication/README.md new file mode 100644 index 0000000..2ed3039 --- /dev/null +++ b/javascript/SimpleSocialMediaApplication/README.md @@ -0,0 +1,77 @@ +# Simple Social Media Application + +A React web application built with Vite. + +## Project Setup + +This project was scaffolded using ViteJS with React template. + +### Prerequisites +- Node.js (version 20.9.0 or higher) +- npm (version 10.4.0 or higher) + +### Installation + +Dependencies are already installed. To install them again: + +```bash +npm install +``` + +### Development + +To run the development server on port 3000: + +```bash +npm run dev +``` + +The application will be available at `http://localhost:3000` + +### Build + +To build the project for production: + +```bash +npm run build +``` + +### Preview + +To preview the production build: + +```bash +npm run preview +``` + +## Project Structure + +``` +SimpleSocialMediaApplication/ +├── public/ +│ └── vite.svg +├── src/ +│ ├── assets/ +│ │ └── react.svg +│ ├── App.css +│ ├── App.jsx +│ ├── index.css +│ └── main.jsx +├── index.html +├── package.json +├── vite.config.js +└── README.md +``` + +## Technologies Used + +- React 19.1.0 +- Vite 7.0.6 +- @vitejs/plugin-react +- Modern JavaScript (ES modules) + +## Configuration + +- **Port**: Configured to run on port 3000 +- **Hot Module Replacement**: Enabled for fast development +- **Build Tool**: Vite for fast builds and development diff --git a/javascript/SimpleSocialMediaApplication/index.html b/javascript/SimpleSocialMediaApplication/index.html new file mode 100644 index 0000000..6d84158 --- /dev/null +++ b/javascript/SimpleSocialMediaApplication/index.html @@ -0,0 +1,13 @@ + + + + + + + Simple Social Media Application + + +
+ + + diff --git a/javascript/SimpleSocialMediaApplication/package-lock.json b/javascript/SimpleSocialMediaApplication/package-lock.json new file mode 100644 index 0000000..1516e31 --- /dev/null +++ b/javascript/SimpleSocialMediaApplication/package-lock.json @@ -0,0 +1,1372 @@ +{ + "name": "simplesocialmediaapplication", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "simplesocialmediaapplication", + "version": "0.0.0", + "dependencies": { + "@vitejs/plugin-react": "^4.7.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "vite": "^5.4.2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.3.tgz", + "integrity": "sha512-8oQkCTve4H4B4JpmD2FV7fV2ZPTxJHN//bRhCqPUU8v6c5APlxteAXyc7BFaEb4aGpUzrPLU4PoAcGhwmRzZTA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.3.tgz", + "integrity": "sha512-StOsmdXHU2hx3UFTTs6yYxCSwSIgLsfjUBICXyWj625M32OOjakXlaZuGKL+jA3Nvv35+hMxrm/64eCoT07SYQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.3.tgz", + "integrity": "sha512-6CfLF3eqKhCdhK0GUnR5ZS99OFz+dtOeB/uePznLKxjCsk5QjT/V0eSEBb4vj+o/ri3i35MseSEQHCLLAgClVw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.3.tgz", + "integrity": "sha512-QLWyWmAJG9elNTNLdcSXUT/M+J7DhEmvs1XPHYcgYkse3UHf9iWTJ+yTPlKMIetiQnNi+cNp+gY4gvjDpREfKw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.3.tgz", + "integrity": "sha512-ZOvBq+5nL0yrZIEo1eq6r7MPvkJ8kC1XATS/yHvcq3WbDNKNKBQ1uIF4hicyzDMoJt72G+sn1nKsFXpifZyRDA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.3.tgz", + "integrity": "sha512-AYvGR07wecEnyYSovyJ71pTOulbNvsrpRpK6i/IM1b0UGX1vFx51afYuPYPxnvE9aCl5xPnhQicEvdIMxClRgQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.3.tgz", + "integrity": "sha512-Yx8Cp38tfRRToVLuIWzBHV25/QPzpUreOPIiUuNV7KahNPurYg2pYQ4l7aYnvpvklO1riX4643bXLvDsYSBIrA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.3.tgz", + "integrity": "sha512-4dIYRNxlXGDKnO6qgcda6LxnObPO6r1OBU9HG8F9pAnHHLtfbiOqCzDvkeHknx+5mfFVH4tWOl+h+cHylwsPWA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.3.tgz", + "integrity": "sha512-M6uVlWKmhLN7LguLDu6396K1W5IBlAaRonjlHQgc3s4dOGceu0FeBuvbXiUPYvup/6b5Ln7IEX7XNm68DN4vrg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.3.tgz", + "integrity": "sha512-emaYiOTQJUd6fC9a6jcw9zIWtzaUiuBC+vomggaM4In2iOra/lA6IMHlqZqQZr08NYXrOPMVigreLMeSAwv3Uw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.3.tgz", + "integrity": "sha512-3P77T5AQ4UfVRJSrTKLiUZDJ6XsxeP80027bp6mOFh8sevSD038mYuIYFiUtrSJxxgFb+NgRJFF9oIa0rlUsmg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.45.3.tgz", + "integrity": "sha512-/VPH3ZVeSlmCBPhZdx/+4dMXDjaGMhDsWOBo9EwSkGbw2+OAqaslL53Ao2OqCxR0GgYjmmssJ+OoG+qYGE7IBg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.3.tgz", + "integrity": "sha512-Hs5if0PjROl1MGMmZX3xMAIfqcGxQE2SJWUr/CpDQsOQn43Wq4IvXXxUMWtiY/BrzdqCCJlRgJ5DKxzS3qWkCw==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.3.tgz", + "integrity": "sha512-Qm0WOwh3Lk388+HJFl1ILGbd2iOoQf6yl4fdGqOjBzEA+5JYbLcwd+sGsZjs5pkt8Cr/1G42EiXmlRp9ZeTvFA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.3.tgz", + "integrity": "sha512-VJdknTaYw+TqXzlh9c7vaVMh/fV2sU8Khfk4a9vAdYXJawpjf6z3U1k7vDWx2IQ9ZOPoOPxgVpDfYOYhxD7QUA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.3.tgz", + "integrity": "sha512-SUDXU5YabLAMl86FpupSQQEWzVG8X0HM+Q/famnJusbPiUgQnTGuSxtxg4UAYgv1ZmRV1nioYYXsgtSokU/7+Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.3.tgz", + "integrity": "sha512-ezmqknOUFgZMN6wW+Avlo4sXF3Frswd+ncrwMz4duyZ5Eqd+dAYgJ+A1MY+12LNZ7XDhCiijJceueYvtnzdviw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.3.tgz", + "integrity": "sha512-1YfXoUEE++gIW66zNB9Twd0Ua5xCXpfYppFUxVT/Io5ZT3fO6Se+C/Jvmh3usaIHHyi53t3kpfjydO2GAy5eBA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.3.tgz", + "integrity": "sha512-Iok2YA3PvC163rVZf2Zy81A0g88IUcSPeU5pOilcbICXre2EP1mxn1Db/l09Z/SK1vdSLtpJXAnwGuMOyf5O9g==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.3.tgz", + "integrity": "sha512-HwHCH5GQTOeGYP5wBEBXFVhfQecwRl24Rugoqhh8YwGarsU09bHhOKuqlyW4ZolZCan3eTUax7UJbGSmKSM51A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.191", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz", + "integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.45.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.3.tgz", + "integrity": "sha512-STwyHZF3G+CrmZhB+qDiROq9s8B5PrOCYN6dtmOvwz585XBnyeHk1GTEhHJtUVb355/9uZhOazyVclTt5uahzA==", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.45.3", + "@rollup/rollup-android-arm64": "4.45.3", + "@rollup/rollup-darwin-arm64": "4.45.3", + "@rollup/rollup-darwin-x64": "4.45.3", + "@rollup/rollup-freebsd-arm64": "4.45.3", + "@rollup/rollup-freebsd-x64": "4.45.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.3", + "@rollup/rollup-linux-arm-musleabihf": "4.45.3", + "@rollup/rollup-linux-arm64-gnu": "4.45.3", + "@rollup/rollup-linux-arm64-musl": "4.45.3", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.3", + "@rollup/rollup-linux-ppc64-gnu": "4.45.3", + "@rollup/rollup-linux-riscv64-gnu": "4.45.3", + "@rollup/rollup-linux-riscv64-musl": "4.45.3", + "@rollup/rollup-linux-s390x-gnu": "4.45.3", + "@rollup/rollup-linux-x64-gnu": "4.45.3", + "@rollup/rollup-linux-x64-musl": "4.45.3", + "@rollup/rollup-win32-arm64-msvc": "4.45.3", + "@rollup/rollup-win32-ia32-msvc": "4.45.3", + "@rollup/rollup-win32-x64-msvc": "4.45.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", + "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.41", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } +} diff --git a/javascript/SimpleSocialMediaApplication/package.json b/javascript/SimpleSocialMediaApplication/package.json new file mode 100644 index 0000000..536552c --- /dev/null +++ b/javascript/SimpleSocialMediaApplication/package.json @@ -0,0 +1,20 @@ +{ + "name": "simplesocialmediaapplication", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@vitejs/plugin-react": "^4.7.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "vite": "^5.4.2" + } +} diff --git a/javascript/SimpleSocialMediaApplication/public/vite.svg b/javascript/SimpleSocialMediaApplication/public/vite.svg new file mode 100644 index 0000000..ee9fada --- /dev/null +++ b/javascript/SimpleSocialMediaApplication/public/vite.svg @@ -0,0 +1 @@ + diff --git a/javascript/SimpleSocialMediaApplication/src/App.css b/javascript/SimpleSocialMediaApplication/src/App.css new file mode 100644 index 0000000..99b24e4 --- /dev/null +++ b/javascript/SimpleSocialMediaApplication/src/App.css @@ -0,0 +1,508 @@ +/* App Layout */ +.app { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.app-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.app-header h1 { + margin: 0; + font-size: 1.8rem; + font-weight: 600; +} + +.app-main { + flex: 1; + padding: 2rem; + max-width: 800px; + margin: 0 auto; + width: 100%; + box-sizing: border-box; +} + +.app-content { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.app-footer { + background: #f8f9fa; + padding: 1rem; + text-align: center; + color: #666; + border-top: 1px solid #e9ecef; +} + +/* Server Status */ +.server-status-indicator { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.status-dot.online { + background: #28a745; + box-shadow: 0 0 8px rgba(40, 167, 69, 0.6); +} + +.status-dot.offline { + background: #dc3545; + box-shadow: 0 0 8px rgba(220, 53, 69, 0.6); +} + +/* Error Display */ +.error-display { + margin-bottom: 1rem; + padding: 1rem; + border-radius: 8px; + position: relative; +} + +.error-display.server-error { + background: #f8d7da; + border: 1px solid #f5c6cb; + color: #721c24; +} + +.error-display.general-error { + background: #fff3cd; + border: 1px solid #ffeaa7; + color: #856404; +} + +.error-content { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.server-status { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; +} + +.status-indicator.offline { + width: 8px; + height: 8px; + background: #dc3545; + border-radius: 50%; +} + +.error-message { + margin: 0; +} + +.dismiss-button { + position: absolute; + top: 0.5rem; + right: 0.5rem; + background: none; + border: none; + font-size: 1.2rem; + cursor: pointer; + color: inherit; +} + +/* Loading Spinner */ +.loading-spinner { + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; +} + +.spinner { + border: 3px solid #f3f3f3; + border-top: 3px solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.loading-spinner.small .spinner { + width: 20px; + height: 20px; +} + +.loading-spinner.medium .spinner { + width: 30px; + height: 30px; +} + +.loading-spinner.large .spinner { + width: 40px; + height: 40px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Form Styles */ +.create-post-form, +.edit-post-form, +.create-comment-form, +.edit-comment-form { + background: white; + padding: 1.5rem; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + border: 1px solid #e9ecef; +} + +.create-post-form h2, +.create-comment-form h4 { + margin: 0 0 1rem 0; + color: #333; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: #555; +} + +.form-group input, +.form-group textarea { + width: 100%; + padding: 0.75rem; + border: 2px solid #e9ecef; + border-radius: 8px; + font-size: 1rem; + transition: border-color 0.2s ease; + box-sizing: border-box; +} + +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.form-group input:disabled, +.form-group textarea:disabled { + background: #f8f9fa; + color: #6c757d; +} + +/* Button Styles */ +.submit-button, +.save-button, +.submit-comment-button, +.like-button, +.unlike-button, +.toggle-comments-button { + background: #667eea; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 8px; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.2s ease; + font-weight: 500; +} + +.submit-button:hover, +.save-button:hover, +.submit-comment-button:hover, +.like-button:hover, +.toggle-comments-button:hover { + background: #5a67d8; +} + +.submit-button:disabled, +.save-button:disabled, +.submit-comment-button:disabled, +.like-button:disabled { + background: #9ca3af; + cursor: not-allowed; +} + +.cancel-button { + background: #6c757d; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 8px; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.2s ease; + font-weight: 500; +} + +.cancel-button:hover { + background: #5a6268; +} + +.edit-button, +.delete-button { + background: none; + border: none; + font-size: 1.2rem; + cursor: pointer; + padding: 0.25rem; + border-radius: 4px; + transition: background-color 0.2s ease; +} + +.edit-button:hover { + background: rgba(102, 126, 234, 0.1); +} + +.delete-button:hover { + background: rgba(220, 53, 69, 0.1); +} + +.unlike-button { + background: #dc3545; + margin-left: 0.5rem; +} + +.unlike-button:hover { + background: #c82333; +} + +.form-actions { + display: flex; + gap: 0.5rem; + margin-top: 1rem; +} + +/* Post Styles */ +.posts-list h2 { + color: #333; + margin-bottom: 1.5rem; +} + +.posts-container { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.post-card { + background: white; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + border: 1px solid #e9ecef; + overflow: hidden; +} + +.post-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 1rem 1.5rem 0.5rem; +} + +.post-meta { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.username { + font-weight: 600; + color: #667eea; + font-size: 1.1rem; +} + +.date, +.updated { + font-size: 0.85rem; + color: #6c757d; +} + +.post-actions { + display: flex; + gap: 0.5rem; +} + +.post-content { + padding: 0 1.5rem; +} + +.post-content p { + margin: 0.5rem 0; + line-height: 1.6; + color: #333; +} + +.post-stats { + padding: 0.5rem 1.5rem; + display: flex; + gap: 1rem; + font-size: 0.9rem; + color: #6c757d; + border-top: 1px solid #f8f9fa; +} + +.post-interactions { + padding: 1rem 1.5rem; + background: #f8f9fa; + border-top: 1px solid #e9ecef; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.like-section { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.like-username-input { + flex: 1; + min-width: 0; + padding: 0.5rem; + border: 1px solid #e9ecef; + border-radius: 6px; + font-size: 0.9rem; +} + +/* Comment Styles */ +.comments-section { + background: #f8f9fa; + border-radius: 8px; + padding: 1rem; + margin-top: 1rem; +} + +.comments-header h3 { + margin: 0 0 1rem 0; + color: #333; + font-size: 1.2rem; +} + +.comments-list { + display: flex; + flex-direction: column; + gap: 1rem; + margin-top: 1rem; +} + +.comment-card { + background: white; + border-radius: 8px; + padding: 1rem; + border: 1px solid #e9ecef; +} + +.comment-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.5rem; +} + +.comment-meta { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.comment-meta .username { + font-size: 0.95rem; +} + +.comment-meta .date, +.comment-meta .updated { + font-size: 0.8rem; +} + +.comment-actions { + display: flex; + gap: 0.5rem; +} + +.comment-content p { + margin: 0; + line-height: 1.5; + color: #333; +} + +.comment-username-input, +.comment-content-input, +.edit-username-input, +.edit-content-input { + width: 100%; + box-sizing: border-box; +} + +/* Loading and Empty States */ +.posts-loading, +.comments-loading { + text-align: center; + padding: 2rem; + color: #6c757d; +} + +.no-posts, +.no-comments { + text-align: center; + padding: 2rem; + color: #6c757d; + font-style: italic; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .app-header { + padding: 1rem; + flex-direction: column; + gap: 0.5rem; + text-align: center; + } + + .app-main { + padding: 1rem; + } + + .post-header, + .comment-header { + flex-direction: column; + gap: 0.5rem; + } + + .post-actions, + .comment-actions { + align-self: flex-end; + } + + .like-section { + flex-direction: column; + align-items: stretch; + } + + .form-actions { + flex-direction: column; + } +} diff --git a/javascript/SimpleSocialMediaApplication/src/App.jsx b/javascript/SimpleSocialMediaApplication/src/App.jsx new file mode 100644 index 0000000..085c8e6 --- /dev/null +++ b/javascript/SimpleSocialMediaApplication/src/App.jsx @@ -0,0 +1,55 @@ +import { useState } from 'react'; +import { AppProvider } from './context/AppContext'; +import ServerStatus from './components/common/ServerStatus'; +import ErrorDisplay from './components/common/ErrorDisplay'; +import CreatePostForm from './components/posts/CreatePostForm'; +import PostsList from './components/posts/PostsList'; +import { useApp } from './context/AppContext'; +import './App.css'; + +const AppContent = () => { + const [refreshTrigger, setRefreshTrigger] = useState(0); + const { error, clearError } = useApp(); + + const handlePostCreated = () => { + // Trigger a refresh of the posts list + setRefreshTrigger(prev => prev + 1); + }; + + return ( +
+
+

Simple Social Media Application

+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+
+ +
+

Built with React and Vite • Backend API: http://localhost:8000

+
+
+ ); +}; + +const App = () => { + return ( + + + + ); +}; + +export default App; diff --git a/javascript/SimpleSocialMediaApplication/src/assets/react.svg b/javascript/SimpleSocialMediaApplication/src/assets/react.svg new file mode 100644 index 0000000..8e0e0f1 --- /dev/null +++ b/javascript/SimpleSocialMediaApplication/src/assets/react.svg @@ -0,0 +1 @@ + diff --git a/javascript/SimpleSocialMediaApplication/src/components/comments/CommentCard.jsx b/javascript/SimpleSocialMediaApplication/src/components/comments/CommentCard.jsx new file mode 100644 index 0000000..d829f33 --- /dev/null +++ b/javascript/SimpleSocialMediaApplication/src/components/comments/CommentCard.jsx @@ -0,0 +1,139 @@ +import { useState } from 'react'; +import { useApp } from '../../context/AppContext'; +import apiService from '../../services/api'; + +const CommentCard = ({ comment, postId, onCommentUpdated, onCommentDeleted }) => { + const [isEditing, setIsEditing] = useState(false); + const [editUsername, setEditUsername] = useState(comment.username); + const [editContent, setEditContent] = useState(comment.content); + const [isUpdating, setIsUpdating] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const { handleError, setServerOnline } = useApp(); + + const handleEdit = () => { + setIsEditing(true); + setEditUsername(comment.username); + setEditContent(comment.content); + }; + + const handleCancelEdit = () => { + setIsEditing(false); + setEditUsername(comment.username); + setEditContent(comment.content); + }; + + const handleUpdate = async (e) => { + e.preventDefault(); + + if (!editUsername.trim() || !editContent.trim()) { + handleError(new Error('Username and content are required')); + return; + } + + setIsUpdating(true); + try { + const updatedComment = await apiService.updateComment(postId, comment.id, { + username: editUsername.trim(), + content: editContent.trim(), + }); + setServerOnline(); + setIsEditing(false); + onCommentUpdated?.(updatedComment); + } catch (error) { + handleError(error); + } finally { + setIsUpdating(false); + } + }; + + const handleDelete = async () => { + if (!window.confirm('Are you sure you want to delete this comment?')) { + return; + } + + setIsDeleting(true); + try { + await apiService.deleteComment(postId, comment.id); + setServerOnline(); + onCommentDeleted?.(comment.id); + } catch (error) { + handleError(error); + } finally { + setIsDeleting(false); + } + }; + + const formatDate = (dateString) => { + return new Date(dateString).toLocaleString(); + }; + + return ( +
+ {isEditing ? ( +
+
+ setEditUsername(e.target.value)} + placeholder="Username" + disabled={isUpdating} + required + className="edit-username-input" + /> +
+
+