diff --git a/package.json b/package.json index ada1684..2e81fa1 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { "name": "femto-webapp", "private": true, - "version": "1.26.6", + "version": "1.23.0", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", - "build:deploy": "bash scripts/publish.sh", + "build:deploy": "bash scripts/bump-build-push.sh", "generate:schema": "node scripts/generate-schema.mjs" }, "dependencies": { @@ -20,8 +20,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.6.0", - "tailwindcss": "^4.1.5", - "zustand": "^5.0.7" + "tailwindcss": "^4.1.5" }, "devDependencies": { "@eslint/js": "^9.22.0", diff --git a/public/forgejo-logo-primary.svg b/public/forgejo-logo-primary.svg deleted file mode 100644 index 7f64c1a..0000000 --- a/public/forgejo-logo-primary.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - Forgejo logo - Caesar Schinas - - - - - - - - - - - - - diff --git a/scripts/bump-build-push.sh b/scripts/bump-build-push.sh new file mode 100755 index 0000000..ae679c6 --- /dev/null +++ b/scripts/bump-build-push.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +set -euo pipefail + +# CONFIGURATION +REGISTRY="docker.botris.dev" +USERNAME="johnbotris" +IMAGE_NAME="femto-webapp" + +# Add this before the docker build line +export VITE_API_URL="https://femto-api.botris.social" + + +# Step 0: Ensure clean working directory +if [[ -n $(git status --porcelain) ]]; then + echo "❌ Uncommitted changes detected. Please commit or stash them before running this script." + exit 1 +fi + +# Step 1: Store current version to revert if needed +OLD_VERSION=$(node -p "require('./package.json').version") +echo "🔍 Current version: $OLD_VERSION" + +# Step 2: Bump version without Git tag/commit +echo "🚀 Bumping minor version..." +yarn version --minor --no-git-tag-version +NEW_VERSION=$(node -p "require('./package.json').version") +echo "📦 New version: $NEW_VERSION" + +# Step 3: Attempt Docker build +echo "🔧 Building Docker image..." + +if ! docker build --build-arg VITE_API_URL="$VITE_API_URL" -t $IMAGE_NAME .; then + echo "❌ Docker build failed. Reverting version bump..." + git checkout -- package.json yarn.lock + exit 1 +fi + +# Step 4: Tag and push Docker image +FULL_IMAGE="$REGISTRY/$USERNAME/$IMAGE_NAME" +echo "🏷️ Tagging Docker image..." +docker tag $IMAGE_NAME $FULL_IMAGE:$NEW_VERSION +docker tag $IMAGE_NAME $FULL_IMAGE:latest + +echo "📤 Pushing Docker images..." +docker push $FULL_IMAGE:$NEW_VERSION +docker push $FULL_IMAGE:latest + +# Step 5: Commit version bump & tag +echo "✅ Committing and tagging version bump..." +git add package.json yarn.lock +git commit -m "v$NEW_VERSION" +git tag "v$NEW_VERSION" +git push origin main +git push origin "v$NEW_VERSION" + +echo "🎉 Release v$NEW_VERSION complete." diff --git a/scripts/publish.sh b/scripts/publish.sh deleted file mode 100755 index 68a75e4..0000000 --- a/scripts/publish.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -# Function to display help text -show_help() { - echo "Usage: $0 [OPTIONS]" - echo - echo "Description:" - echo " This script automates the process of bumping the version, building a Docker image," - echo " pushing it to the registry, and optionally deploying to production." - echo - echo "Options:" - echo " -h,--help Display this help message and exit" - echo " -d,--deploy Deploy to production after building and pushing" - echo " --major Bump the major version (x.0.0)" - echo " --minor Bump the minor version (0.x.0)" - echo " --patch Bump the patch version (0.0.x) [default]" - echo - echo "Examples:" - echo " $0 # Bump patch version, build and push" - echo " $0 --minor # Bump minor version, build and push" - echo " $0 --major -d # Bump major version, build, push and deploy" - echo " $0 --patch --deploy # Bump patch version, build, push and deploy" - echo -} - -# Parse command line arguments -DEPLOY=false -VERSION_TYPE="patch" # Default to patch version bump - -for arg in "$@"; do - case "$arg" in - -h|--help) show_help; exit 0 ;; - -d|--deploy) DEPLOY=true ;; - --major) VERSION_TYPE="major" ;; - --minor) VERSION_TYPE="minor" ;; - --patch) VERSION_TYPE="patch" ;; - *) echo "Unknown option: $arg"; echo "Usage: $0 [-h|--help] [-d|--deploy] [--major|--minor|--patch]"; exit 1 ;; - esac -done - -# CONFIGURATION -REGISTRY="docker.botris.dev" -USERNAME="johnbotris" -IMAGE_NAME="femto-webapp" - -# Add this before the docker build line -export VITE_API_URL="https://api.botris.social" - -# Step 0: Ensure clean working directory -if [[ -n $(git status --porcelain) ]]; then - echo "❌ Uncommitted changes detected. Please commit or stash them before running this script." - exit 1 -fi - -# Step 1: Store current version to revert if needed -OLD_VERSION=$(node -p "require('./package.json').version") -echo "🔍 Current version: $OLD_VERSION" - -# Step 2: Bump version without Git tag/commit -echo "🚀 Bumping $VERSION_TYPE version..." -yarn version --$VERSION_TYPE --no-git-tag-version -NEW_VERSION=$(node -p "require('./package.json').version") -echo "📦 New version: $NEW_VERSION" - -# Step 3: Attempt Docker build -echo "🔧 Building Docker image..." - -if ! docker build --build-arg VITE_API_URL="$VITE_API_URL" -t $IMAGE_NAME .; then - echo "❌ Docker build failed. Reverting version bump..." - git checkout -- package.json yarn.lock - exit 1 -fi - -# Step 4: Tag and push Docker image -FULL_IMAGE="$REGISTRY/$USERNAME/$IMAGE_NAME" -echo "🏷️ Tagging Docker image..." -docker tag $IMAGE_NAME $FULL_IMAGE:$NEW_VERSION -docker tag $IMAGE_NAME $FULL_IMAGE:latest - -echo "📤 Pushing Docker images..." -docker push $FULL_IMAGE:$NEW_VERSION -docker push $FULL_IMAGE:latest - -# Step 5: Commit version bump & tag -echo "✅ Committing and tagging version bump..." -git add package.json yarn.lock -git commit -m "v$NEW_VERSION" -git tag "v$NEW_VERSION" -git push origin main -git push origin "v$NEW_VERSION" - -echo "🎉 Release v$NEW_VERSION complete." - -# Step 6: Deploy if flag is set -if [ "$DEPLOY" = true ]; then - echo "🚀 Deploying to production..." - ssh john@botris.social 'bash /home/john/docker/femto/update.sh' - echo "✅ Deployment complete." -fi diff --git a/src/App.tsx b/src/App.tsx index 9063592..3694fdf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,5 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom' import HomePage from './app/feed/pages/HomePage.tsx' -import PostPage from './app/feed/pages/PostPage.tsx' import SignupPage from './app/auth/pages/SignupPage.tsx' import LoginPage from './app/auth/pages/LoginPage.tsx' import LogoutPage from './app/auth/pages/LogoutPage.tsx' @@ -8,36 +7,13 @@ import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx' import AdminPage from './app/admin/pages/AdminPage.tsx' import SignupCodesManagementPage from './app/admin/pages/subpages/SignupCodesManagementPage.tsx' import { useRefreshSessionLoop } from './useRefreshSessionLoop.ts' -import { setGlobal } from './app/femtoApp.ts' -import { PostsService } from './app/feed/posts/postsService.ts' -import { MediaService } from './app/media/mediaService.ts' -import { AuthService } from './app/auth/authService.ts' -import { initClient } from './app/api/client.ts' -import { useEffect } from 'react' -import { useUserStore } from './app/user/user.ts' -import { getUserFromCookie } from './app/auth/getUserFromCookie.ts' +import { initApp } from './initApp.ts' -setGlobal('version', import.meta.env.VITE_FEMTO_VERSION) - -const client = initClient() -const postService = new PostsService(client) -const mediaService = new MediaService(client) -const authService = new AuthService(client) - -setGlobal('postsService', postService) -setGlobal('authService', authService) +const { postService, mediaService, authService } = initApp() export default function App() { - const setUser = useUserStore((state) => state.setUser) - useRefreshSessionLoop(authService) - useEffect(() => { - const user = getUserFromCookie() - console.debug('got user cookie', user) - setUser(user) - }, [setUser]) - return ( @@ -46,7 +22,6 @@ export default function App() { path={'/'} element={} /> - } /> } /> } /> } /> diff --git a/src/app/api/client.ts b/src/app/api/client.ts index eebacdf..63cce0c 100644 --- a/src/app/api/client.ts +++ b/src/app/api/client.ts @@ -1,8 +1,6 @@ import { paths } from './schema.ts' import createClient, { Middleware } from 'openapi-fetch' import { dispatchMessage } from '../messageBus/messageBus.ts' -import { useUserStore } from '../user/user.ts' -import { getUserFromCookie } from '../auth/getUserFromCookie.ts' export const initClient = () => { const client = createClient({ baseUrl: import.meta.env.VITE_API_URL }) @@ -11,10 +9,6 @@ export const initClient = () => { if (response.status === 401) { dispatchMessage('auth:unauthorized', null) } - - const user = getUserFromCookie() - console.debug('got user cookie', user) - useUserStore.getState().setUser(user) }, } diff --git a/src/app/api/schema.ts b/src/app/api/schema.ts index 6af29b3..b809ef0 100644 --- a/src/app/api/schema.ts +++ b/src/app/api/schema.ts @@ -9,7 +9,7 @@ export interface paths { get: { parameters: { query?: { - After?: string + From?: string Amount?: number AuthorId?: string Author?: string @@ -75,30 +75,7 @@ export interface paths { path?: never cookie?: never } - get: { - parameters: { - query?: never - header?: never - path: { - postId: string - } - cookie?: never - } - requestBody?: never - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'text/plain': components['schemas']['GetPostResponse'] - 'application/json': components['schemas']['GetPostResponse'] - 'text/json': components['schemas']['GetPostResponse'] - } - } - } - } + get?: never put?: never post?: never delete: { @@ -192,47 +169,6 @@ export interface paths { patch?: never trace?: never } - '/posts/{postId}/comments': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - post: { - parameters: { - query?: never - header?: never - path: { - postId: string - } - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['AddPostCommentRequest'] - 'text/json': components['schemas']['AddPostCommentRequest'] - 'application/*+json': components['schemas']['AddPostCommentRequest'] - } - } - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } '/media': { parameters: { query?: never @@ -252,8 +188,7 @@ export interface paths { requestBody: { content: { 'multipart/form-data': { - /** Format: binary */ - file?: string + file?: components['schemas']['IFormFile'] } } } @@ -398,78 +333,6 @@ export interface paths { patch?: never trace?: never } - '/auth/change-password': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - post: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['ChangePasswordRequestBody'] - 'text/json': components['schemas']['ChangePasswordRequestBody'] - 'application/*+json': components['schemas']['ChangePasswordRequestBody'] - } - } - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/auth/delete-current-session': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - post: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } '/auth/session': { parameters: { query?: never @@ -527,9 +390,9 @@ export interface paths { [name: string]: unknown } content: { - 'text/plain': components['schemas']['GetUserInfoResult'] - 'application/json': components['schemas']['GetUserInfoResult'] - 'text/json': components['schemas']['GetUserInfoResult'] + 'text/plain': components['schemas']['RefreshUserResult'] + 'application/json': components['schemas']['RefreshUserResult'] + 'text/json': components['schemas']['RefreshUserResult'] } } } @@ -602,99 +465,13 @@ export interface paths { patch?: never trace?: never } - '/auth/create-signup-code': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - post: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['CreateSignupCodeRequest'] - 'text/json': components['schemas']['CreateSignupCodeRequest'] - 'application/*+json': components['schemas']['CreateSignupCodeRequest'] - } - } - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/auth/list-signup-codes': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'text/plain': components['schemas']['ListSignupCodesResult'] - 'application/json': components['schemas']['ListSignupCodesResult'] - 'text/json': components['schemas']['ListSignupCodesResult'] - } - } - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } } export type webhooks = Record export interface components { schemas: { - AddPostCommentRequest: { - /** Format: uuid */ - authorId: string - content: string - } AddPostReactionRequest: { emoji: string } - ChangePasswordRequestBody: { - /** Format: uuid */ - userId: string - newPassword: string - } CreatePostRequest: { /** Format: uuid */ authorId: string @@ -724,25 +501,20 @@ export interface components { DeletePostReactionRequest: { emoji: string } - GetPostResponse: { - post: components['schemas']['PostDto'] - } - GetUserInfoResult: { - /** Format: uuid */ - userId: string - username: string - isSuperUser: boolean - } + /** Format: binary */ + IFormFile: string ListSignupCodesResult: { signupCodes: components['schemas']['SignupCodeDto'][] } LoadPostsResponse: { posts: components['schemas']['PostDto'][] + /** Format: uuid */ + next: string | null } LoginRequest: { username: string password: string - rememberMe: boolean + rememberMe: boolean | null } LoginResponse: { /** Format: uuid */ @@ -755,12 +527,6 @@ export interface components { authorId: string username: string } - PostCommentDto: { - author: string - content: string - /** Format: date-time */ - postedOn: string - } PostDto: { author: components['schemas']['PostAuthorDto'] /** Format: uuid */ @@ -771,7 +537,6 @@ export interface components { /** Format: date-time */ createdAt: string possibleReactions: string[] - comments: components['schemas']['PostCommentDto'][] } PostMediaDto: { /** Format: uri */ @@ -783,15 +548,21 @@ export interface components { } PostReactionDto: { emoji: string - authorName: string - /** Format: date-time */ - reactedOn: string + /** Format: int32 */ + count: number + didReact: boolean + } + RefreshUserResult: { + /** Format: uuid */ + userId: string + username: string + isSuperUser: boolean } RegisterRequest: { username: string password: string signupCode: string - rememberMe: boolean + rememberMe: boolean | null } RegisterResponse: { /** Format: uuid */ diff --git a/src/app/auth/components/AuthNavButtons.tsx b/src/app/auth/components/AuthNavButtons.tsx index 0c5141b..c646888 100644 --- a/src/app/auth/components/AuthNavButtons.tsx +++ b/src/app/auth/components/AuthNavButtons.tsx @@ -1,11 +1,11 @@ +import { useUser } from '../../user/user.ts' import NavButton from '../../../components/buttons/NavButton.tsx' import { useLocation } from 'react-router-dom' import { useTranslations } from '../../i18n/translations.ts' -import { useUserStore } from '../../user/user.ts' export default function AuthNavButtons() { const { t } = useTranslations() - const user = useUserStore((state) => state.user) + const user = useUser() const { pathname } = useLocation() diff --git a/src/app/auth/components/Protected.tsx b/src/app/auth/components/Protected.tsx index 19d71bb..f5ea238 100644 --- a/src/app/auth/components/Protected.tsx +++ b/src/app/auth/components/Protected.tsx @@ -1,9 +1,9 @@ +import { useUser } from '../../user/user.ts' import { useNavigate, Outlet } from 'react-router-dom' import { useEffect } from 'react' -import { useUserStore } from '../../user/user.ts' export default function Protected() { - const user = useUserStore((state) => state.user) + const user = useUser() const navigate = useNavigate() diff --git a/src/app/auth/components/RefreshUser.tsx b/src/app/auth/components/RefreshUser.tsx index 7d1db08..8796381 100644 --- a/src/app/auth/components/RefreshUser.tsx +++ b/src/app/auth/components/RefreshUser.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren, useEffect, useRef } from 'react' import { AuthService } from '../authService.ts' -import { useUserStore } from '../../user/user.ts' +import { useUser } from '../../user/user.ts' interface RefreshUserProps { authService: AuthService @@ -10,7 +10,7 @@ export default function RefreshUser({ authService, children, }: PropsWithChildren) { - const user = useUserStore((state) => state.user) + const user = useUser() const didRefresh = useRef(false) useEffect(() => { diff --git a/src/app/auth/getUserFromCookie.ts b/src/app/auth/getUserFromCookie.ts deleted file mode 100644 index 00bd891..0000000 --- a/src/app/auth/getUserFromCookie.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { User } from '../user/user.ts' -import { getCookie } from './cookies.ts' - -export function getUserFromCookie(): User | null { - const userCookie = getCookie('user') - - if (!userCookie) return null - - // TODO validate but it should be fine - return JSON.parse(decodeURIComponent(userCookie)) as User -} diff --git a/src/app/auth/pages/LoginPage.tsx b/src/app/auth/pages/LoginPage.tsx index 8b914ca..3838a88 100644 --- a/src/app/auth/pages/LoginPage.tsx +++ b/src/app/auth/pages/LoginPage.tsx @@ -4,7 +4,7 @@ import TextInput from '../../../components/inputs/TextInput.tsx' import Button from '../../../components/buttons/Button.tsx' import { AuthService } from '../authService.ts' import { useNavigate } from 'react-router-dom' -import { useUserStore } from '../../user/user.ts' +import { useUser } from '../../user/user.ts' import NavBar from '../../../components/NavBar.tsx' import NavButton from '../../../components/buttons/NavButton.tsx' import LinkButton from '../../../components/buttons/LinkButton.tsx' @@ -26,7 +26,7 @@ export default function LoginPage({ authService }: LoginPageProps) { const passwordInputRef = useRef(null) const navigate = useNavigate() - const user = useUserStore((state) => state.user) + const user = useUser() useEffect(() => { if (user) { @@ -95,11 +95,11 @@ export default function LoginPage({ authService }: LoginPageProps) { id="password" value={password} onInput={setPassword} - className={'mb-4'} + className={'mb-3'} /> -
+
- {error} + {error}
diff --git a/src/app/auth/pages/LogoutPage.tsx b/src/app/auth/pages/LogoutPage.tsx index dfceb5c..dea8a72 100644 --- a/src/app/auth/pages/LogoutPage.tsx +++ b/src/app/auth/pages/LogoutPage.tsx @@ -1,7 +1,7 @@ import { useNavigate } from 'react-router-dom' import { AuthService } from '../authService.ts' import { useEffect } from 'react' -import { useUserStore } from '../../user/user.ts' +import { useUser } from '../../user/user.ts' interface LogoutPageProps { authService: AuthService @@ -9,7 +9,7 @@ interface LogoutPageProps { export default function LogoutPage({ authService }: LogoutPageProps) { const navigate = useNavigate() - const user = useUserStore((state) => state.user) + const user = useUser() useEffect(() => { if (!user) { diff --git a/src/app/auth/pages/SignupPage.tsx b/src/app/auth/pages/SignupPage.tsx index 40cbb37..2e3a200 100644 --- a/src/app/auth/pages/SignupPage.tsx +++ b/src/app/auth/pages/SignupPage.tsx @@ -114,7 +114,7 @@ export default function SignupPage({ authService }: SignupPageProps) { type="password" ref={passwordInputRef} /> -
+
void - isSubmitting?: boolean -} - -export default function NewCommentWidget({ - onSubmit, - isSubmitting = false, -}: NewCommentWidgetProps) { - const { t } = useTranslations() - const [content, setContent] = useState('') - - const onContentInput = (value: string) => { - setContent(value) - } - - const handleSubmit = () => { - if (!content.trim()) { - return - } - - onSubmit(content) - - setContent('') - } - - const onInputKeyDown = (e: TextInputKeyDownEvent) => { - if (e.key === 'Enter' && e.ctrlKey) { - e.preventDefault() - handleSubmit() - } - } - - return ( -
- - -
- -
-
- ) -} diff --git a/src/app/feed/components/PostItem.tsx b/src/app/feed/components/PostItem.tsx index db766f6..a0a541c 100644 --- a/src/app/feed/components/PostItem.tsx +++ b/src/app/feed/components/PostItem.tsx @@ -1,24 +1,13 @@ -import { PostMedia, PostReaction } from '../posts/posts.ts' +import { Post, PostMedia } from '../posts/posts.ts' import { useEffect, useState } from 'react' -import { Link } from 'react-router-dom' -import { PostInfo } from '../posts/usePostViewModel.ts' -import { useUserStore } from '../../user/user.ts' interface PostItemProps { - post: PostInfo - reactions: PostReaction[] + post: Post addReaction: (emoji: string) => void clearReaction: (emoji: string) => void - hideViewButton?: boolean } -export default function PostItem({ - post, - reactions, - addReaction, - clearReaction, - hideViewButton = false, -}: PostItemProps) { +export default function PostItem({ post, addReaction, clearReaction }: PostItemProps) { const formattedDate = post.createdAt.toLocaleString('en-US', { year: 'numeric', month: 'short', @@ -42,14 +31,6 @@ export default function PostItem({
@{post.authorName}• {formattedDate} - {!hideViewButton && ( - <> - {' • '} - - View - - - )}
{post.content}
@@ -62,30 +43,26 @@ export default function PostItem({
)} - + ) } interface PostReactionsProps { - post: PostInfo - reactions: PostReaction[] + post: Post addReaction: (emoji: string) => void clearReaction: (emoji: string) => void } -function PostReactions({ post, reactions, addReaction, clearReaction }: PostReactionsProps) { - const username = useUserStore((state) => state.user?.username) +function PostReactions({ post, addReaction, clearReaction }: PostReactionsProps) { + const reactionMap = new Map(post.reactions.map((r) => [r.emoji, r])) + return (
{post.possibleReactions.map((emoji) => { - const count = reactions.filter((r) => r.emoji === emoji).length - const didReact = reactions.some((r) => r.emoji == emoji && r.authorName == username) + const reaction = reactionMap.get(emoji) + const count = reaction?.count ?? 0 + const didReact = reaction?.didReact ?? false const onClick = () => { if (didReact) { clearReaction(emoji) @@ -122,7 +99,7 @@ function PostReactionButton({ emoji, didReact, onClick, count }: PostReactionBut