feat: add SPA, serve at /app/, update Dockerfile and README

- React + TanStack Router + shadcn/ui SPA under spa/
- serve spa/dist at /app/ with index.html fallback for client routing
- Dockerfile: node build stage for SPA, copy dist into runtime image
- README: document SPA, CORS_ORIGINS env var, architecture entry
- vite base set to /app/, manifest.json paths fixed
This commit is contained in:
2026-06-04 04:20:15 +02:00
parent 15dc0e526b
commit b9c0b10740
153 changed files with 24329 additions and 1 deletions

300
spa/src/locales/en.json Normal file
View File

@@ -0,0 +1,300 @@
{
"common": {
"cancel": "Cancel",
"confirm": "Confirm",
"delete": "Delete",
"save": "Save",
"saving": "Saving...",
"back": "Back",
"tryAgain": "Try again",
"follow": "Follow",
"following": "Following",
"unfollow": "Unfollow",
"remove": "Remove",
"block": "Block",
"unblock": "Unblock",
"accept": "Accept",
"reject": "Reject",
"dismiss": "Dismiss",
"continue": "Continue",
"generate": "Generate",
"generating": "Generating...",
"reviews": "{{count}} reviews",
"films": "{{count}} films",
"filmsAvg": "{{count}} films, avg {{avg}}"
},
"nav": {
"home": "Home",
"search": "Search",
"diary": "Diary",
"profile": "Profile"
},
"auth": {
"title": "Movies Diary",
"loginHeading": "Log in to your account",
"registerHeading": "Create your account",
"email": "Email",
"username": "Username",
"password": "Password",
"loginError": "Invalid email or password",
"registerError": "Registration failed. Try a different email.",
"loggingIn": "Logging in...",
"logIn": "Log in",
"creating": "Creating...",
"createAccount": "Create account",
"noAccount": "Don't have an account?",
"hasAccount": "Already have an account?"
},
"errors": {
"somethingWrong": "Something went wrong",
"unknownError": "Unknown error"
},
"feed": {
"title": "Movies Diary",
"tab": "Feed",
"watchlist": "Watchlist",
"queue": "Queue",
"sortLatest": "Latest",
"sortOldest": "Oldest",
"sortTopRated": "Top Rated",
"sortLowestRated": "Lowest Rated",
"noActivity": "No activity yet",
"noActivityDesc": "Follow people to see their reviews",
"addToWatchlist": "Add to watchlist",
"watchlistEmpty": "Watchlist empty",
"watchlistEmptyDesc": "Save movies you want to watch",
"removeFromWatchlist": "Remove from watchlist?",
"queueEmpty": "Queue empty",
"queueEmptyDesc": "Movies from Jellyfin/Plex appear here",
"deleteReview": "Delete this review?"
},
"diary": {
"title": "Diary",
"noEntries": "No entries",
"nothingLogged": "Nothing logged this month",
"deleteReview": "Delete this review?"
},
"movie": {
"ratingDistribution": "Rating Distribution",
"community": "Community",
"yourHistory": "Your History",
"castCrew": "Cast & Crew",
"saved": "Saved",
"watchlist": "Watchlist",
"noReviews": "No reviews yet",
"beFirst": "Be the first to review",
"trend": "Trend: {{trend}}",
"noViewings": "No viewings",
"notLogged": "You haven't logged this movie",
"tmdbStats": "TMDB Stats",
"budget": "Budget",
"revenue": "Revenue",
"tmdb": "TMDB",
"cast": "Cast",
"crew": "Crew",
"keywords": "Keywords",
"castCredits": "Cast Credits ({{count}})",
"crewCredits": "Crew Credits ({{count}})",
"noCredits": "No credits found"
},
"search": {
"placeholder": "Search movies, people...",
"searchPrompt": "Search for movies or people",
"noResults": "No results",
"noResultsDesc": "Try a different search term",
"people": "People",
"movies": "Movies"
},
"profile": {
"title": "Profile",
"followingFollowers": "Following & Followers",
"yearInReview": "Year in Review",
"recent": "Recent",
"topRated": "Top Rated",
"trends": "Trends",
"movies": "Movies",
"avg": "Avg",
"followingStat": "Following",
"followers": "Followers",
"noEntries": "No entries",
"noTrends": "No trends yet",
"topDirectors": "Top Directors",
"monthlyActivity": "Monthly Activity"
},
"social": {
"title": "Social",
"following": "Following",
"followers": "Followers",
"pending": "Pending",
"notFollowing": "Not following anyone",
"notFollowingDesc": "Follow users to see their reviews in your feed",
"noFollowers": "No followers yet",
"noFollowersOther": "No followers",
"noPending": "No pending requests",
"followSent": "Follow request sent to {{handle}}",
"followError": "Could not follow that user",
"handlePlaceholder": "@user@instance.example"
},
"settings": {
"title": "Settings",
"editProfile": "Edit Profile",
"editProfileDesc": "Username, bio",
"import": "Import",
"importDesc": "Import from CSV",
"yearWrapUp": "Year Wrap-Up",
"yearWrapUpDesc": "Annual summaries",
"webhookTokens": "Webhook Tokens",
"webhookTokensDesc": "Jellyfin, Plex",
"blockedUsers": "Blocked Users",
"blockedUsersDesc": "Manage blocked users",
"blockedUsersDescAdmin": "Users & domains",
"blockedDomains": "Blocked Domains",
"blockedDomainsDesc": "Federation blocks",
"logOut": "Log Out",
"account": "Account",
"data": "Data",
"integrations": "Integrations",
"socialGroup": "Social"
},
"editProfile": {
"title": "Edit Profile",
"banner": "Banner",
"avatar": "Avatar",
"displayName": "Display Name",
"displayNamePlaceholder": "How you appear to others",
"bio": "Bio",
"bioPlaceholder": "Tell us about yourself",
"federation": "Federation",
"federationDesc": "ActivityPub profile settings",
"alsoKnownAs": "Also Known As",
"alsoKnownAsPlaceholder": "https://other-instance.example/users/you",
"alsoKnownAsHelp": "URL of your account on another instance, for account migration",
"profileFields": "Profile Fields",
"profileFieldsHelp": "Key-value pairs shown on your profile (e.g. Website, Pronouns)",
"label": "Label",
"value": "Value",
"addField": "Add field"
},
"blocked": {
"title": "Blocked",
"users": "Users",
"domains": "Domains",
"noBlockedUsers": "No blocked users",
"noBlockedDomains": "No blocked domains",
"domainPlaceholder": "example.com"
},
"webhooks": {
"title": "Webhook Tokens",
"noTokens": "No tokens",
"noTokensDesc": "Generate one to get started",
"generateToken": "Generate Token",
"provider": "Provider",
"jellyfin": "Jellyfin",
"plex": "Plex",
"labelOptional": "Label (optional)",
"labelPlaceholder": "e.g. Living room",
"copied": "Webhook URL copied to clipboard"
},
"wrapup": {
"title": "Year Wrap-Up",
"noWrapUps": "No wrap-ups",
"generateWrapUp": "Generate Wrap-Up",
"startDate": "Start Date",
"endDate": "End Date",
"heroSubtitle": "Your Year in Movies",
"moviesWatched": "movies watched",
"watchHours": "{{hours}} hours of watch time",
"ratings": "Ratings",
"averageRating": "average rating",
"busiestMonth": "Busiest month: {{month}}",
"favoriteDay": "Favorite day: {{day}}",
"topDirectors": "Top Directors",
"uniqueDirectors": "{{count}} unique directors",
"topActors": "Top Actors",
"uniqueActors": "{{count}} unique actors",
"genres": "Genres",
"genresExplored": "{{count}} genres explored",
"highestRated": "Highest rated: {{genre}}",
"lowestRated": "Lowest rated: {{genre}}",
"highlights": "Highlights",
"highlightHighest": "Highest Rated",
"highlightLowest": "Lowest Rated",
"highlightOldest": "Oldest Film",
"highlightNewest": "Newest Film",
"highlightLongest": "Longest",
"highlightShortest": "Shortest",
"highlightFirst": "First Watched",
"highlightLast": "Last Watched",
"rewatches": "Rewatches",
"moviesRewatched": "movies rewatched",
"mostRewatched": "Most rewatched:",
"posterMosaic": "Your Year in Posters"
},
"logReview": {
"title": "Log Review",
"yourRating": "Your Rating",
"commentPlaceholder": "Add a comment... (optional)",
"logging": "Logging...",
"logReview": "Log Review",
"logged": "{{title}} logged!"
},
"searchOverlay": {
"backToSearch": "Back to search",
"addManuallyTitle": "Add movie manually",
"addManuallyDesc": "The backend will try to match this on TMDB automatically",
"imdbId": "IMDb / TMDB ID",
"imdbPlaceholder": "e.g. tt0816692",
"imdbHelp": "Exact lookup — fill just this to add by ID",
"orSearchByTitle": "Or search by title:",
"titleLabel": "Title",
"titlePlaceholder": "e.g. Interstellar",
"releaseYear": "Release year",
"yearPlaceholder": "e.g. 2014",
"director": "Director",
"directorPlaceholder": "e.g. Christopher Nolan",
"searchPlaceholder": "Search movies...",
"noMoviesFound": "No movies found",
"addManually": "Add manually",
"addManuallySubtitle": "Movie not in the database yet"
},
"swipeToDelete": {
"removeItem": "Remove this item?"
},
"import": {
"title": "Import",
"step": "Step {{current}} of {{total}}",
"fieldTitle": "Title",
"fieldReleaseYear": "Release Year",
"fieldDirector": "Director",
"fieldRating": "Rating",
"fieldWatchedAt": "Watched At",
"fieldComment": "Comment",
"fieldExternalId": "External ID",
"fieldSkip": "Skip",
"scale1to5": "15 (keep as-is)",
"scale1to10": "110 → 15 (×0.5)",
"scale1to100": "1100 → 15 (×0.05)",
"scaleLetterboxd": "04 Letterboxd → 15 (×1.25)",
"dropCsv": "Drop a CSV file or tap to browse",
"uploading": "Uploading...",
"preview": "Preview",
"rowsCols": "{{rows}} rows · {{cols}} columns",
"columnMapping": "Column Mapping",
"ratingScale": "Rating Scale",
"ratingScaleDesc": "How should ratings be converted to 15?",
"dateFormat": "Date Format",
"dateFormatDesc": "Leave empty for auto-detection",
"dateFormatPlaceholder": "e.g. %Y-%m-%d",
"applying": "Applying...",
"importSummary": "Import Summary",
"summaryDesc": "{{valid}} valid · {{duplicates}} duplicates · {{invalid}} invalid",
"previewRows": "Preview ({{count}} rows)",
"status": "Status",
"year": "Year",
"watched": "Watched",
"importing": "Importing...",
"importRows": "Import {{count}} rows",
"importComplete": "Import complete!",
"viewDiary": "View your diary"
}
}