mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-31 19:59:31 -05:00
feat(frontend/api): i18n support
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import type { MovieResult, TvResult } from '../../../server/models/Search';
|
||||
import TitleCard from '../TitleCard';
|
||||
@@ -6,6 +6,14 @@ import { MediaRequest } from '../../../server/entity/MediaRequest';
|
||||
import RequestCard from '../TitleCard/RequestCard';
|
||||
import Slider from '../Slider';
|
||||
import Link from 'next/link';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { LanguageContext } from '../../context/LanguageContext';
|
||||
|
||||
const messages = defineMessages({
|
||||
recentrequests: 'Recent Requests',
|
||||
popularmovies: 'Popular Movies',
|
||||
populartv: 'Popular Series',
|
||||
});
|
||||
|
||||
interface MovieDiscoverResult {
|
||||
page: number;
|
||||
@@ -22,11 +30,12 @@ interface TvDiscoverResult {
|
||||
}
|
||||
|
||||
const Discover: React.FC = () => {
|
||||
const { locale } = useContext(LanguageContext);
|
||||
const { data: movieData, error: movieError } = useSWR<MovieDiscoverResult>(
|
||||
'/api/v1/discover/movies'
|
||||
`/api/v1/discover/movies?language=${locale}`
|
||||
);
|
||||
const { data: tvData, error: tvError } = useSWR<TvDiscoverResult>(
|
||||
'/api/v1/discover/tv'
|
||||
`/api/v1/discover/tv?language=${locale}`
|
||||
);
|
||||
|
||||
const { data: requests, error: requestError } = useSWR<MediaRequest[]>(
|
||||
@@ -39,7 +48,9 @@ const Discover: React.FC = () => {
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link href="/requests">
|
||||
<a className="inline-flex text-xl leading-7 text-cool-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
|
||||
<span>Recent Requests</span>
|
||||
<span>
|
||||
<FormattedMessage {...messages.recentrequests} />
|
||||
</span>
|
||||
<svg
|
||||
className="w-6 h-6 ml-2"
|
||||
fill="none"
|
||||
@@ -74,7 +85,9 @@ const Discover: React.FC = () => {
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link href="/discover/movies">
|
||||
<a className="inline-flex text-xl leading-7 text-cool-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
|
||||
<span>Popular Movies</span>
|
||||
<span>
|
||||
<FormattedMessage {...messages.popularmovies} />
|
||||
</span>
|
||||
<svg
|
||||
className="w-6 h-6 ml-2"
|
||||
fill="none"
|
||||
@@ -116,7 +129,9 @@ const Discover: React.FC = () => {
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link href="/discover/tv">
|
||||
<a className="inline-flex text-xl leading-7 text-cool-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
|
||||
<span>Popular TV Shows</span>
|
||||
<span>
|
||||
<FormattedMessage {...messages.populartv} />
|
||||
</span>
|
||||
<svg
|
||||
className="w-6 h-6 ml-2"
|
||||
fill="none"
|
||||
|
||||
109
src/components/Layout/LanguagePicker/index.tsx
Normal file
109
src/components/Layout/LanguagePicker/index.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React, { useState, useRef, useContext } from 'react';
|
||||
import Transition from '../../Transition';
|
||||
import useClickOutside from '../../../hooks/useClickOutside';
|
||||
import {
|
||||
LanguageContext,
|
||||
AvailableLocales,
|
||||
} from '../../../context/LanguageContext';
|
||||
import { FormattedMessage, defineMessages } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
changelanguage: 'Change Language',
|
||||
});
|
||||
|
||||
type AvailableLanguageObject = Record<
|
||||
string,
|
||||
{ code: AvailableLocales; display: string }
|
||||
>;
|
||||
|
||||
const availableLanguages: AvailableLanguageObject = {
|
||||
en: {
|
||||
code: 'en',
|
||||
display: 'English',
|
||||
},
|
||||
ja: {
|
||||
code: 'ja',
|
||||
display: '日本語',
|
||||
},
|
||||
};
|
||||
|
||||
const LanguagePicker: React.FC = () => {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const { locale, setLocale } = useContext(LanguageContext);
|
||||
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
||||
useClickOutside(dropdownRef, () => setDropdownOpen(false));
|
||||
|
||||
return (
|
||||
<div className="ml-3 relative">
|
||||
<div>
|
||||
<button
|
||||
className="p-1 text-gray-400 rounded-full hover:bg-cool-gray-500 hover:text-white focus:outline-none focus:shadow-outline focus:text-white"
|
||||
aria-label="Language Picker"
|
||||
onClick={() => setDropdownOpen(true)}
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M7 2a1 1 0 011 1v1h3a1 1 0 110 2H9.578a18.87 18.87 0 01-1.724 4.78c.29.354.596.696.914 1.026a1 1 0 11-1.44 1.389c-.188-.196-.373-.396-.554-.6a19.098 19.098 0 01-3.107 3.567 1 1 0 01-1.334-1.49 17.087 17.087 0 003.13-3.733 18.992 18.992 0 01-1.487-2.494 1 1 0 111.79-.89c.234.47.489.928.764 1.372.417-.934.752-1.913.997-2.927H3a1 1 0 110-2h3V3a1 1 0 011-1zm6 6a1 1 0 01.894.553l2.991 5.982a.869.869 0 01.02.037l.99 1.98a1 1 0 11-1.79.895L15.383 16h-4.764l-.724 1.447a1 1 0 11-1.788-.894l.99-1.98.019-.038 2.99-5.982A1 1 0 0113 8zm-1.382 6h2.764L13 11.236 11.618 14z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<Transition
|
||||
show={isDropdownOpen}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg"
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<div className="py-2 px-2 rounded-md bg-cool-gray-700 shadow-xs">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="language"
|
||||
className="block text-sm leading-5 font-medium text-cool-gray-300 pb-2"
|
||||
>
|
||||
<FormattedMessage {...messages.changelanguage} />
|
||||
</label>
|
||||
<select
|
||||
id="language"
|
||||
className="mt-1 form-select block w-full pl-3 pr-10 py-2 text-base leading-6 text-white bg-cool-gray-700 border-cool-gray-600 focus:outline-none focus:shadow-outline-indigo focus:border-blue-800 sm:text-sm sm:leading-5"
|
||||
onChange={(e) =>
|
||||
setLocale && setLocale(e.target.value as AvailableLocales)
|
||||
}
|
||||
onBlur={(e) =>
|
||||
setLocale && setLocale(e.target.value as AvailableLocales)
|
||||
}
|
||||
>
|
||||
{(Object.keys(
|
||||
availableLanguages
|
||||
) as (keyof typeof availableLanguages)[]).map((key) => (
|
||||
<option
|
||||
key={key}
|
||||
value={availableLanguages[key].code}
|
||||
selected={locale === availableLanguages[key].code}
|
||||
>
|
||||
{availableLanguages[key].display}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguagePicker;
|
||||
@@ -3,6 +3,7 @@ import SearchInput from './SearchInput';
|
||||
import UserDropdown from './UserDropdown';
|
||||
import Sidebar from './Sidebar';
|
||||
import Notifications from './Notifications';
|
||||
import LanguagePicker from './LanguagePicker';
|
||||
|
||||
const Layout: React.FC = ({ children }) => {
|
||||
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
||||
@@ -35,6 +36,7 @@ const Layout: React.FC = ({ children }) => {
|
||||
<div className="flex-1 px-4 flex justify-between">
|
||||
<SearchInput />
|
||||
<div className="ml-4 flex items-center md:ml-6">
|
||||
<LanguagePicker />
|
||||
<Notifications />
|
||||
<UserDropdown />
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useContext } from 'react';
|
||||
import {
|
||||
FormattedMessage,
|
||||
defineMessages,
|
||||
FormattedNumber,
|
||||
FormattedDate,
|
||||
} from 'react-intl';
|
||||
import type { MovieDetails as MovieDetailsType } from '../../../server/models/Movie';
|
||||
import useSWR from 'swr';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -12,6 +18,19 @@ import Link from 'next/link';
|
||||
import Slider from '../Slider';
|
||||
import TitleCard from '../TitleCard';
|
||||
import PersonCard from '../PersonCard';
|
||||
import { LanguageContext } from '../../context/LanguageContext';
|
||||
|
||||
const messages = defineMessages({
|
||||
releasedate: 'Release Date',
|
||||
userrating: 'User Rating',
|
||||
status: 'Status',
|
||||
revenue: 'Revenue',
|
||||
budget: 'Budget',
|
||||
originallanguage: 'Original Language',
|
||||
overview: 'Overview',
|
||||
runtime: '{minutes} minutes',
|
||||
cast: 'Cast',
|
||||
});
|
||||
|
||||
interface MovieDetailsProps {
|
||||
movie?: MovieDetailsType;
|
||||
@@ -33,20 +52,21 @@ enum MediaRequestStatus {
|
||||
|
||||
const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
const router = useRouter();
|
||||
const { locale } = useContext(LanguageContext);
|
||||
const { addToast } = useToasts();
|
||||
const [showRequestModal, setShowRequestModal] = useState(false);
|
||||
const [showCancelModal, setShowCancelModal] = useState(false);
|
||||
const { data, error, revalidate } = useSWR<MovieDetailsType>(
|
||||
`/api/v1/movie/${router.query.movieId}`,
|
||||
`/api/v1/movie/${router.query.movieId}?language=${locale}`,
|
||||
{
|
||||
initialData: movie,
|
||||
}
|
||||
);
|
||||
const { data: recommended, error: recommendedError } = useSWR<SearchResult>(
|
||||
`/api/v1/movie/${router.query.movieId}/recommendations`
|
||||
`/api/v1/movie/${router.query.movieId}/recommendations?language=${locale}`
|
||||
);
|
||||
const { data: similar, error: similarError } = useSWR<SearchResult>(
|
||||
`/api/v1/movie/${router.query.movieId}/similar`
|
||||
`/api/v1/movie/${router.query.movieId}/similar?language=${locale}`
|
||||
);
|
||||
|
||||
const request = async () => {
|
||||
@@ -110,7 +130,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
<img
|
||||
src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`}
|
||||
alt=""
|
||||
className="rounded shadow md:shadow-2xl w-32 md:w-52"
|
||||
className="rounded md:rounded-lg shadow md:shadow-2xl w-32 md:w-52"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left">
|
||||
@@ -119,7 +139,11 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
</span>
|
||||
<h1 className="text-2xl md:text-4xl">{data.title}</h1>
|
||||
<span className="text-xs md:text-base mt-1 md:mt-0">
|
||||
{data.runtime} minutes | {data.genres.map((g) => g.name).join(', ')}
|
||||
<FormattedMessage
|
||||
{...messages.runtime}
|
||||
values={{ minutes: data.runtime }}
|
||||
/>{' '}
|
||||
| {data.genres.map((g) => g.name).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 flex justify-end mt-4 md:mt-0">
|
||||
@@ -249,31 +273,70 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
</div>
|
||||
<div className="flex pt-8 text-white flex-col md:flex-row pb-4">
|
||||
<div className="flex-1 md:mr-8">
|
||||
<h2 className="text-xl md:text-2xl">Overview</h2>
|
||||
<h2 className="text-xl md:text-2xl">
|
||||
<FormattedMessage {...messages.overview} />
|
||||
</h2>
|
||||
<p className="pt-2 text-sm md:text-base">{data.overview}</p>
|
||||
</div>
|
||||
<div className="w-full md:w-80 mt-8 md:mt-0">
|
||||
<div className="bg-cool-gray-900 rounded-lg shadow border border-cool-gray-800">
|
||||
<div className="flex px-4 py-2 border-b border-cool-gray-800 last:border-b-0">
|
||||
<span className="text-sm">Status</span>
|
||||
<span className="text-sm">
|
||||
<FormattedMessage {...messages.userrating} />
|
||||
</span>
|
||||
<span className="flex-1 text-right text-cool-gray-400 text-sm">
|
||||
{data.voteAverage}/10
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex px-4 py-2 border-b border-cool-gray-800 last:border-b-0">
|
||||
<span className="text-sm">
|
||||
<FormattedMessage {...messages.releasedate} />
|
||||
</span>
|
||||
<span className="flex-1 text-right text-cool-gray-400 text-sm">
|
||||
<FormattedDate
|
||||
value={new Date(data.releaseDate)}
|
||||
year="numeric"
|
||||
month="long"
|
||||
day="numeric"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex px-4 py-2 border-b border-cool-gray-800 last:border-b-0">
|
||||
<span className="text-sm">
|
||||
<FormattedMessage {...messages.status} />
|
||||
</span>
|
||||
<span className="flex-1 text-right text-cool-gray-400 text-sm">
|
||||
{data.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex px-4 py-2 border-b border-cool-gray-800 last:border-b-0">
|
||||
<span className="text-sm">Revenue</span>
|
||||
<span className="text-sm">
|
||||
<FormattedMessage {...messages.revenue} />
|
||||
</span>
|
||||
<span className="flex-1 text-right text-cool-gray-400 text-sm">
|
||||
{data.revenue}
|
||||
<FormattedNumber
|
||||
currency="USD"
|
||||
style="currency"
|
||||
value={data.revenue}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex px-4 py-2 border-b border-cool-gray-800 last:border-b-0">
|
||||
<span className="text-sm">Budget</span>
|
||||
<span className="text-sm">
|
||||
<FormattedMessage {...messages.budget} />
|
||||
</span>
|
||||
<span className="flex-1 text-right text-cool-gray-400 text-sm">
|
||||
{data.budget}
|
||||
<FormattedNumber
|
||||
currency="USD"
|
||||
style="currency"
|
||||
value={data.budget}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex px-4 py-2 border-b border-cool-gray-800 last:border-b-0">
|
||||
<span className="text-sm">Original Language</span>
|
||||
<span className="text-sm">
|
||||
<FormattedMessage {...messages.originallanguage} />
|
||||
</span>
|
||||
<span className="flex-1 text-right text-cool-gray-400 text-sm">
|
||||
{data.originalLanguage}
|
||||
</span>
|
||||
@@ -285,7 +348,9 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link href="/movie/[movieId]/cast" as={`/movie/${data.id}/cast`}>
|
||||
<a className="inline-flex text-xl leading-7 text-cool-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
|
||||
<span>Cast</span>
|
||||
<span>
|
||||
<FormattedMessage {...messages.cast} />
|
||||
</span>
|
||||
<svg
|
||||
className="w-6 h-6 ml-2"
|
||||
fill="none"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import type { MovieDetails } from '../../../server/models/Movie';
|
||||
import type { TvDetails } from '../../../server/models/Tv';
|
||||
import TitleCard from '.';
|
||||
import { LanguageContext } from '../../context/LanguageContext';
|
||||
|
||||
interface TmdbTitleCardProps {
|
||||
tmdbId: number;
|
||||
@@ -14,9 +15,12 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||
};
|
||||
|
||||
const RequestCard: React.FC<TmdbTitleCardProps> = ({ tmdbId, type }) => {
|
||||
const { locale } = useContext(LanguageContext);
|
||||
const url =
|
||||
type === 'movie' ? `/api/v1/movie/${tmdbId}` : `/api/v1/tv/${tmdbId}`;
|
||||
const { data: title, error } = useSWR<MovieDetails | TvDetails>(url);
|
||||
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
||||
`${url}?language=${locale}`
|
||||
);
|
||||
|
||||
if (!title && !error) {
|
||||
return <TitleCard.Placeholder />;
|
||||
|
||||
15
src/context/LanguageContext.tsx
Normal file
15
src/context/LanguageContext.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
export type AvailableLocales = 'en' | 'ja';
|
||||
|
||||
interface LanguageContextProps {
|
||||
locale: AvailableLocales;
|
||||
children: (locale: string) => ReactNode;
|
||||
setLocale?: React.Dispatch<React.SetStateAction<AvailableLocales>>;
|
||||
}
|
||||
|
||||
export const LanguageContext = React.createContext<
|
||||
Omit<LanguageContextProps, 'children'>
|
||||
>({
|
||||
locale: 'en',
|
||||
});
|
||||
15
src/i18n/locale/en.json
Normal file
15
src/i18n/locale/en.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"components.Discover.popularmovies": "Popular Movies",
|
||||
"components.Discover.populartv": "Popular Series",
|
||||
"components.Discover.recentrequests": "Recent Requests",
|
||||
"components.Layout.LanguagePicker.changelanguage": "Change Language",
|
||||
"components.MovieDetails.budget": "Budget",
|
||||
"components.MovieDetails.cast": "Cast",
|
||||
"components.MovieDetails.originallanguage": "Original Language",
|
||||
"components.MovieDetails.overview": "Overview",
|
||||
"components.MovieDetails.releasedate": "Release Date",
|
||||
"components.MovieDetails.revenue": "Revenue",
|
||||
"components.MovieDetails.runtime": "{minutes} minutes",
|
||||
"components.MovieDetails.status": "Status",
|
||||
"components.MovieDetails.userrating": "User Rating"
|
||||
}
|
||||
15
src/i18n/locale/ja.json
Normal file
15
src/i18n/locale/ja.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"components.Discover.popularmovies": "人気映画",
|
||||
"components.Discover.populartv": "人気テレビ番組",
|
||||
"components.Discover.recentrequests": "最近のリクエスト",
|
||||
"components.Layout.LanguagePicker.changelanguage": "言語",
|
||||
"components.MovieDetails.budget": "興行収入",
|
||||
"components.MovieDetails.cast": "キャスト",
|
||||
"components.MovieDetails.originallanguage": "言語",
|
||||
"components.MovieDetails.overview": "ストーリー",
|
||||
"components.MovieDetails.releasedate": "公開日",
|
||||
"components.MovieDetails.revenue": "製作費",
|
||||
"components.MovieDetails.runtime": "{minutes}分",
|
||||
"components.MovieDetails.status": "状態",
|
||||
"components.MovieDetails.userrating": "ユーザー評価"
|
||||
}
|
||||
@@ -1,82 +1,136 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import '../styles/globals.css';
|
||||
import App, { AppInitialProps } from 'next/app';
|
||||
import App, { AppInitialProps, AppProps } from 'next/app';
|
||||
import { SWRConfig } from 'swr';
|
||||
import { ToastProvider } from 'react-toast-notifications';
|
||||
import { parseCookies, setCookie } from 'nookies';
|
||||
import Layout from '../components/Layout';
|
||||
import { UserContext } from '../context/UserContext';
|
||||
import axios from 'axios';
|
||||
import { User } from '../hooks/useUser';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { LanguageContext, AvailableLocales } from '../context/LanguageContext';
|
||||
|
||||
const loadLocaleData = (locale: string) => {
|
||||
switch (locale) {
|
||||
case 'ja':
|
||||
return import('../i18n/locale/ja.json');
|
||||
default:
|
||||
return import('../i18n/locale/en.json');
|
||||
}
|
||||
};
|
||||
|
||||
// Custom types so we can correctly type our GetInitialProps function
|
||||
// with our combined user prop
|
||||
// This is specific to _app.tsx. Other pages will not need to do this!
|
||||
type NextAppComponentType = typeof App;
|
||||
type GetInitialPropsFn = NextAppComponentType['getInitialProps'];
|
||||
type MessagesType = Record<string, any>;
|
||||
|
||||
interface AppProps {
|
||||
interface ExtendedAppProps extends AppProps {
|
||||
user: User;
|
||||
messages: MessagesType;
|
||||
locale: AvailableLocales;
|
||||
}
|
||||
|
||||
class CoreApp extends App<AppProps> {
|
||||
public static getInitialProps: GetInitialPropsFn = async (initialProps) => {
|
||||
// Run the default getInitialProps for the main nextjs initialProps
|
||||
const appInitialProps: AppInitialProps = await App.getInitialProps(
|
||||
initialProps
|
||||
);
|
||||
const { ctx, router } = initialProps;
|
||||
let user = undefined;
|
||||
if (ctx.res) {
|
||||
try {
|
||||
// Attempt to get the user by running a request to the local api
|
||||
const response = await axios.get<User>(
|
||||
`http://localhost:${process.env.PORT || 3000}/api/v1/auth/me`,
|
||||
{ headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined }
|
||||
);
|
||||
user = response.data;
|
||||
} catch (e) {
|
||||
// If there is no user, and ctx.res is set (to check if we are on the server side)
|
||||
// _AND_ we are not already on the login or setup route, redirect to /login with a 307
|
||||
// before anything actually renders
|
||||
if (!router.pathname.match(/(login|setup)/)) {
|
||||
ctx.res.writeHead(307, {
|
||||
Location: '/login',
|
||||
});
|
||||
ctx.res.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof window === 'undefined') {
|
||||
global.Intl = require('intl');
|
||||
}
|
||||
|
||||
return { ...appInitialProps, user };
|
||||
};
|
||||
const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
|
||||
Component,
|
||||
pageProps,
|
||||
router,
|
||||
user,
|
||||
messages,
|
||||
locale,
|
||||
}: ExtendedAppProps) => {
|
||||
let component: React.ReactNode;
|
||||
const [loadedMessages, setMessages] = useState<MessagesType>(messages);
|
||||
const [currentLocale, setLocale] = useState<AvailableLocales>(locale);
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { Component, pageProps, router, user } = this.props;
|
||||
useEffect(() => {
|
||||
loadLocaleData(currentLocale).then(setMessages);
|
||||
setCookie(null, 'locale', currentLocale, { path: '/' });
|
||||
}, [currentLocale]);
|
||||
|
||||
let component: React.ReactNode;
|
||||
|
||||
if (router.asPath === '/login') {
|
||||
component = <Component {...pageProps} />;
|
||||
} else {
|
||||
component = (
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
fetcher: (url) => axios.get(url).then((res) => res.data),
|
||||
}}
|
||||
>
|
||||
<ToastProvider>
|
||||
<UserContext initialUser={user}>{component}</UserContext>
|
||||
</ToastProvider>
|
||||
</SWRConfig>
|
||||
if (router.asPath === '/login') {
|
||||
component = <Component {...pageProps} />;
|
||||
} else {
|
||||
component = (
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
fetcher: (url) => axios.get(url).then((res) => res.data),
|
||||
}}
|
||||
>
|
||||
<LanguageContext.Provider value={{ locale: currentLocale, setLocale }}>
|
||||
<IntlProvider
|
||||
locale={currentLocale}
|
||||
defaultLocale="en"
|
||||
messages={loadedMessages}
|
||||
>
|
||||
<ToastProvider>
|
||||
<UserContext initialUser={user}>{component}</UserContext>
|
||||
</ToastProvider>
|
||||
</IntlProvider>
|
||||
</LanguageContext.Provider>
|
||||
</SWRConfig>
|
||||
);
|
||||
};
|
||||
|
||||
CoreApp.getInitialProps = async (initialProps) => {
|
||||
// Run the default getInitialProps for the main nextjs initialProps
|
||||
const appInitialProps: AppInitialProps = await App.getInitialProps(
|
||||
initialProps
|
||||
);
|
||||
const { ctx, router } = initialProps;
|
||||
let user = undefined;
|
||||
|
||||
let locale = 'en';
|
||||
|
||||
if (ctx.res) {
|
||||
const cookies = parseCookies(ctx);
|
||||
|
||||
if (cookies.locale) {
|
||||
locale = cookies.locale;
|
||||
}
|
||||
|
||||
try {
|
||||
// Attempt to get the user by running a request to the local api
|
||||
const response = await axios.get<User>(
|
||||
`http://localhost:${process.env.PORT || 3000}/api/v1/auth/me`,
|
||||
{ headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined }
|
||||
);
|
||||
user = response.data;
|
||||
|
||||
if (router.pathname.match(/login/)) {
|
||||
ctx.res.writeHead(307, {
|
||||
Location: '/',
|
||||
});
|
||||
ctx.res.end();
|
||||
}
|
||||
} catch (e) {
|
||||
// If there is no user, and ctx.res is set (to check if we are on the server side)
|
||||
// _AND_ we are not already on the login or setup route, redirect to /login with a 307
|
||||
// before anything actually renders
|
||||
if (!router.pathname.match(/(login|setup)/)) {
|
||||
ctx.res.writeHead(307, {
|
||||
Location: '/login',
|
||||
});
|
||||
ctx.res.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const messages = await loadLocaleData(locale);
|
||||
|
||||
return { ...appInitialProps, user, messages, locale };
|
||||
};
|
||||
|
||||
export default CoreApp;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NextPage } from 'next';
|
||||
import type { MovieDetails as MovieDetailsType } from '../../../../server/models/Movie';
|
||||
import MovieDetails from '../../../components/MovieDetails';
|
||||
import axios from 'axios';
|
||||
import { parseCookies } from 'nookies';
|
||||
|
||||
interface MoviePageProps {
|
||||
movie?: MovieDetailsType;
|
||||
@@ -14,10 +15,11 @@ const MoviePage: NextPage<MoviePageProps> = ({ movie }) => {
|
||||
|
||||
MoviePage.getInitialProps = async (ctx) => {
|
||||
if (ctx.req) {
|
||||
const cookies = parseCookies(ctx);
|
||||
const response = await axios.get<MovieDetailsType>(
|
||||
`http://localhost:${process.env.PORT || 3000}/api/v1/movie/${
|
||||
ctx.query.movieId
|
||||
}`,
|
||||
}?language=${cookies.locale}`,
|
||||
{ headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined }
|
||||
);
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NextPage } from 'next';
|
||||
import PersonCard from '../components/PersonCard';
|
||||
|
||||
const PlexText: NextPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<PersonCard />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlexText;
|
||||
11
src/types/react-intl-auto.d.ts
vendored
Normal file
11
src/types/react-intl-auto.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { MessageDescriptor } from 'react-intl';
|
||||
|
||||
declare module 'react-intl' {
|
||||
interface ExtractableMessage {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export function defineMessages<T extends ExtractableMessage>(
|
||||
messages: T
|
||||
): { [K in keyof T]: MessageDescriptor };
|
||||
}
|
||||
Reference in New Issue
Block a user