diff --git a/ras/api/modules/challenges.py b/ras/api/modules/challenges.py new file mode 100644 index 0000000..1a1fd4e --- /dev/null +++ b/ras/api/modules/challenges.py @@ -0,0 +1,122 @@ +import jwt, json +from django.contrib.auth import get_user_model +from rest_framework.decorators import api_view +import ras.settings +from sqlalchemy import create_engine +from sqlalchemy.sql import text +from django.http import JsonResponse +import pandas as pd +from rest_framework.response import Response + +def filterData(df, datayear = None): + df['readed'] = pd.to_datetime(df['readed'], format='%Y-%m-%d') + df['readed'] = df['readed'].dt.strftime('%m-%Y') + + # Filter data on year + if datayear and datayear is not None: + df = df.where(df['readed'].str.contains(datayear)) + + return df + +def getBooksData(): + engine = create_engine('mysql+mysqldb://' + ras.settings.DATABASES['default']['USER'] + ':' + ras.settings.DATABASES['default']['PASSWORD'] + '@' + ras.settings.DATABASES['default']['HOST'] + ':3306/' + ras.settings.DATABASES['default']['NAME']) + df = pd.read_sql('SELECT * FROM api_books ORDER BY readed', engine, parse_dates={'readed': {'format': '%m-%Y'}}) + + return df + +def getBookChallenge(year = None): + engine = create_engine('mysql+mysqldb://' + ras.settings.DATABASES['default']['USER'] + ':' + ras.settings.DATABASES['default']['PASSWORD'] + '@' + ras.settings.DATABASES['default']['HOST'] + ':3306/' + ras.settings.DATABASES['default']['NAME']) + if(year): + df = pd.read_sql('SELECT * FROM book_challenge where year = ' + year, engine) + else: + df = pd.read_sql('SELECT * FROM book_challenge', engine) + + return df + +@api_view(['GET']) +def getAllChallenges(request): + data = [] + df = getBookChallenge() + + for index, row in df.iterrows(): + + books = filterData(getBooksData(), str(row['year'])) + books = books.dropna() + + totalBooksRead = books['name'].count() + + data.append({ + "id": row['id'], + "year": row['year'], + "nrofbooks": row['nrofbooks'], + "booksread": totalBooksRead + }) + + return Response(data) + +@api_view(['GET']) +def getChallengeOfYear(request): + if request.META.get('HTTP_YEAR'): + data = [] + df = getBookChallenge(request.META.get('HTTP_YEAR')) + + for index, row in df.iterrows(): + data.append({ + "year": row['year'], + "nrofbooks": row['nrofbooks'] + }) + + return Response(data) + else: + return Response("No year header included") + +@api_view(['POST']) +def addChallenge(request): + if(request.headers.get('Authorization')): + token = request.headers.get('Authorization').split(' ')[1] + + try: + User = get_user_model() + payload = jwt.decode(token, 'secret', algorithms=['HS256']) + user = User.objects.get(id=payload['id']) + + if(user): + year = request.POST.get('year') + challenge = request.POST.get('challenge') + + if(year and challenge): + engine = create_engine('mysql+mysqldb://' + ras.settings.DATABASES['default']['USER'] + ':' + ras.settings.DATABASES['default']['PASSWORD'] + '@' + ras.settings.DATABASES['default']['HOST'] + ':3306/' + ras.settings.DATABASES['default']['NAME']) + conn = engine.connect() + conn.execute(text("INSERT INTO book_challenge (year, nrofbooks) VALUES ('" + str(year) + "', '" + str(challenge) + "')")) + return JsonResponse("OK", safe=False) + else: + return JsonResponse({'error': 'No year and challenge detected'}, safe=False) + else: + return JsonResponse({'error': 'No user detected'}, safe=False) + except (jwt.DecodeError, User.DoesNotExist): + return JsonResponse({'error': 'Token invalid'}, safe=False) + +@api_view(['DELETE']) +def deleteChallenge(request, id = None): + + if(request.headers.get('Authorization')): + token = request.headers.get('Authorization').split(' ')[1] + + try: + User = get_user_model() + payload = jwt.decode(token, 'secret', algorithms=['HS256']) + user = User.objects.get(id=payload['id']) + + if(user): + + if(id): + engine = create_engine('mysql+mysqldb://' + ras.settings.DATABASES['default']['USER'] + ':' + ras.settings.DATABASES['default']['PASSWORD'] + '@' + ras.settings.DATABASES['default']['HOST'] + ':3306/' + ras.settings.DATABASES['default']['NAME']) + conn = engine.connect() + conn.execute(text("DELETE FROM book_challenge WHERE id = " + str(id))) + return JsonResponse("OK", safe=False) + else: + return JsonResponse({'error': 'No challengeid detected'}, safe=False) + else: + return JsonResponse({'error': 'No user detected'}, safe=False) + except (jwt.DecodeError, User.DoesNotExist): + return JsonResponse({'error': 'Token invalid'}, safe=False) \ No newline at end of file diff --git a/ras/api/modules/crud.py b/ras/api/modules/crud.py index 518b914..af1a4c5 100644 --- a/ras/api/modules/crud.py +++ b/ras/api/modules/crud.py @@ -8,33 +8,53 @@ from django.http import JsonResponse import pandas as pd from rest_framework.response import Response -def getBooksData(): - engine = create_engine('mysql+mysqldb://' + ras.settings.DATABASES['default']['USER'] + ':' + ras.settings.DATABASES['default']['PASSWORD'] + '@' + ras.settings.DATABASES['default']['HOST'] + ':3306/' + ras.settings.DATABASES['default']['NAME']) - df = pd.read_sql('SELECT * FROM api_books ORDER BY readed', engine, parse_dates={'readed': {'format': '%m-%Y'}}) +engine = create_engine('mysql+mysqldb://' + ras.settings.DATABASES['default']['USER'] + ':' + ras.settings.DATABASES['default']['PASSWORD'] + '@' + ras.settings.DATABASES['default']['HOST'] + ':3306/' + ras.settings.DATABASES['default']['NAME']) +conn = engine.connect() - return df +# ------------------------------- +# Get all books in the database +# ------------------------------- @api_view(['GET']) def getAllBooks(request): + if(request.headers.get('Authorization')): + token = request.headers.get('Authorization').split(' ')[1] - data = [] - books = getBooksData() + try: + User = get_user_model() + payload = jwt.decode(token, 'secret', algorithms=['HS256']) + user = User.objects.get(id=payload['id']) - for index, row in books.iterrows(): - data.append({ - "id": row['id'], - "name": row['name'], - "author": row['author'], - "genre": row['genre'], - "author": row['author'], - "country": row['country'], - "country_code": row['country_code'], - "pages": row['pages'], - "readed": row['readed'], - "rating": row['rating'], - }) + if(user): + books = pd.read_sql('SELECT * FROM api_books ORDER BY readed', engine, parse_dates={'readed': {'format': '%m-%Y'}}) + data = [] - return Response(data) + for index, row in books.iterrows(): + data.append({ + "id": row['id'], + "name": row['name'], + "author": row['author'], + "genre": row['genre'], + "author": row['author'], + "country": row['country'], + "country_code": row['country_code'], + "pages": row['pages'], + "readed": row['readed'], + "rating": row['rating'], + }) + + return Response(data) + else: + return JsonResponse({'error': 'No user detected'}, safe=False) + + except (jwt.DecodeError, User.DoesNotExist): + return JsonResponse({'error': 'Token invalid'}, safe=False) + else: + return JsonResponse({'error': 'Unauthorized'}, safe=False) + +# ------------------------------- +# Add a book into the database +# ------------------------------- @api_view(['POST']) def addBook(request): @@ -49,8 +69,6 @@ def addBook(request): user = User.objects.get(id=payload['id']) if(user): - engine = create_engine('mysql+mysqldb://' + ras.settings.DATABASES['default']['USER'] + ':' + ras.settings.DATABASES['default']['PASSWORD'] + '@' + ras.settings.DATABASES['default']['HOST'] + ':3306/' + ras.settings.DATABASES['default']['NAME']) - conn = engine.connect() conn.execute(text("INSERT INTO api_books (name, author, genre, country, country_code, pages, readed, rating) VALUES ('" + str(book['name']) + "', '" + str(book['author']) + "', '" + str(book['genre']) + "', '" + str(book['country']) + "', '" + str(book['country_code']) + "', " + str(book['pages']) + ", '" + str(book['readed']) + "', " + str(book['rating']) + ")")) return JsonResponse("OK", safe=False) else: @@ -59,7 +77,11 @@ def addBook(request): except (jwt.DecodeError, User.DoesNotExist): return JsonResponse({'error': 'Token invalid'}, safe=False) else: - return JsonResponse({'error': 'testing'}, safe=False) + return JsonResponse({'error': 'Unauthorized'}, safe=False) + +# ------------------------------- +# Update a book in the database +# ------------------------------- @api_view(['PUT']) def updateBook(request): @@ -75,10 +97,7 @@ def updateBook(request): user = User.objects.get(id=payload['id']) if(user): - engine = create_engine('mysql+mysqldb://' + ras.settings.DATABASES['default']['USER'] + ':' + ras.settings.DATABASES['default']['PASSWORD'] + '@' + ras.settings.DATABASES['default']['HOST'] + ':3306/' + ras.settings.DATABASES['default']['NAME']) - conn = engine.connect() conn.execute(text("UPDATE api_books set name='" + str(book['name']) + "', author='" + str(book['author']) + "', genre='" + str(book['genre']) + "', country='" + str(book['country']) + "', country_code='" + str(book['country_code']) + "', pages='" + str(book['pages']) + "', readed='" + str(book['readed']) + "', rating='" + str(book['rating']) + "' WHERE id=" + str(bookid))) - return JsonResponse("OK", safe=False) else: return JsonResponse({'error': 'No user detected'}, safe=False) @@ -86,7 +105,12 @@ def updateBook(request): except (jwt.DecodeError, User.DoesNotExist): return JsonResponse({'error': 'Token invalid'}, safe=False) else: - return JsonResponse({'error': 'No Token'}, safe=False) + return JsonResponse({'error': 'Unauthorized'}, safe=False) + + +# ------------------------------- +# Delete a book in the database +# ------------------------------- @api_view(['DELETE']) def deleteBook(request): @@ -100,8 +124,6 @@ def deleteBook(request): user = User.objects.get(id=payload['id']) if(user): - engine = create_engine('mysql+mysqldb://' + ras.settings.DATABASES['default']['USER'] + ':' + ras.settings.DATABASES['default']['PASSWORD'] + '@' + ras.settings.DATABASES['default']['HOST'] + ':3306/' + ras.settings.DATABASES['default']['NAME']) - conn = engine.connect() conn.execute(text("DELETE FROM api_books WHERE id = " + str(bookid))) return JsonResponse("OK", safe=False) else: @@ -110,4 +132,4 @@ def deleteBook(request): except (jwt.DecodeError, User.DoesNotExist): return JsonResponse({'error': 'Token invalid'}, safe=False) else: - return JsonResponse({'error': 'No Token'}, safe=False) \ No newline at end of file + return JsonResponse({'error': 'Unauthorized'}, safe=False) \ No newline at end of file diff --git a/ras/api/modules/functions.py b/ras/api/modules/functions.py new file mode 100644 index 0000000..02924d7 --- /dev/null +++ b/ras/api/modules/functions.py @@ -0,0 +1,37 @@ +from sqlalchemy import create_engine +import ras.settings +import pandas as pd +from django.contrib.auth import get_user_model +import jwt +from django.http import JsonResponse + +def isAuthorized(authtoken): + if(authtoken): + token = authtoken.split(' ')[1] + + User = get_user_model() + payload = jwt.decode(token, 'secret', algorithms=['HS256']) + user = User.objects.get(id=payload['id']) + + if(user): + return True; + else: + return False; + else: + return JsonResponse({'error': 'Unauthorized'}, safe=False) + +def getBooksData(): + engine = create_engine('mysql+mysqldb://' + ras.settings.DATABASES['default']['USER'] + ':' + ras.settings.DATABASES['default']['PASSWORD'] + '@' + ras.settings.DATABASES['default']['HOST'] + ':3306/' + ras.settings.DATABASES['default']['NAME']) + df = pd.read_sql('SELECT * FROM api_books ORDER BY readed', engine, parse_dates={'readed': {'format': '%m-%Y'}}) + + return df + +def filterData(df, datayear = None): + df['readed'] = pd.to_datetime(df['readed'], format='%Y-%m-%d') + df['readed'] = df['readed'].dt.strftime('%m-%Y') + + # Filter data on year + if datayear and datayear is not None: + df = df.where(df['readed'].str.contains(datayear)) + + return df \ No newline at end of file diff --git a/ras/api/modules/pandas.py b/ras/api/modules/pandas.py index 75376b6..6e115fc 100644 --- a/ras/api/modules/pandas.py +++ b/ras/api/modules/pandas.py @@ -1,191 +1,182 @@ from rest_framework.decorators import api_view -from sqlalchemy import create_engine -import ras.settings import pandas as pd +import math from rest_framework.response import Response +from django.http import JsonResponse +from .functions import isAuthorized, getBooksData, filterData -def getBooksData(): - engine = create_engine('mysql+mysqldb://' + ras.settings.DATABASES['default']['USER'] + ':' + ras.settings.DATABASES['default']['PASSWORD'] + '@' + ras.settings.DATABASES['default']['HOST'] + ':3306/' + ras.settings.DATABASES['default']['NAME']) - df = pd.read_sql('SELECT * FROM api_books ORDER BY readed', engine, parse_dates={'readed': {'format': '%m-%Y'}}) - - return df - -def filterData(df, datayear = None): - df['readed'] = pd.to_datetime(df['readed'], format='%Y-%m-%d') - df['readed'] = df['readed'].dt.strftime('%m-%Y') - - # Filter data on year - if datayear and datayear is not None: - df = df.where(df['readed'].str.contains(datayear)) - - return df +# ---------------------- +# Get all reading years +# ---------------------- @api_view(['GET']) def getYears(request): - df = filterData(getBooksData()) + if(request.headers.get('Authorization')): + isLoggedIn = isAuthorized(request.headers.get('Authorization')); - df['readed'] = pd.to_datetime(df['readed'], errors='coerce') - df['year']= df['readed'].dt.year + if(isLoggedIn): + df = filterData(getBooksData()) + df['readed'] = pd.to_datetime(df['readed'], errors='coerce') + df['year']= df['readed'].dt.year + years = df.groupby('year')['year'].count().reset_index(name="count") - years = df.groupby('year')['year'].count().reset_index(name="count") + return Response(years['year']) + else: + return JsonResponse({'error': 'Unauthorized'}, safe=False) + else: + return JsonResponse({'error': 'No authorization token'}, safe=False) - return Response(years['year']) - -@api_view(['GET']) -def getBooksByYear(request): - if request.META.get('HTTP_YEAR'): - data = [] - df = getBooksData() - df['readed'] = pd.to_datetime(df['readed'], format='%Y-%m-%d') - df['readed'] = df['readed'].dt.strftime('%Y-%m-%d') - df = df.where(df['readed'].str.contains(request.META.get('HTTP_YEAR'))) - df = df.fillna('') - - for index, row in df.iterrows(): - if row['id'] and row['id'] != '': - data.append({ - "id": row['id'], - "name": row['name'], - "author": row['author'], - "genre": row['genre'], - "author": row['author'], - "country": row['country'], - "country_code": row['country_code'], - "pages": row['pages'], - "readed": row['readed'], - "rating": row['rating'], - }) - - return Response(data) +# ------------------------------------------------------------------ +# Get books of selected year and filter it per month and per genre +# ------------------------------------------------------------------ @api_view(['GET']) def books_per_genre_per_month(request): - if request.META.get('HTTP_YEAR'): + if(request.headers.get('Authorization')): + isLoggedIn = isAuthorized(request.headers.get('Authorization')); - data = [] - df = filterData(getBooksData(), request.META.get('HTTP_YEAR')) + if(isLoggedIn): + if request.META.get('HTTP_YEAR'): + data = [] + df = filterData(getBooksData(), request.META.get('HTTP_YEAR')) + booksPerMonth = df.groupby(['genre','readed'])['genre'].count().reset_index(name="count") + booksPerMonth = booksPerMonth.sort_values(by=['genre', 'readed', 'count'], ascending=False) - # Filter array on genre and date - booksPerMonth = df.groupby(['genre','readed'])['genre'].count().reset_index(name="count") - booksPerMonth = booksPerMonth.sort_values(by=['genre', 'readed', 'count'], ascending=False) - - for index, row in booksPerMonth.iterrows(): - data.append({ - "genre": row['genre'], - "readed": row['readed'], - "count": row['count'] - }) - - return Response(data) + for index, row in booksPerMonth.iterrows(): + data.append({ + "genre": row['genre'], + "readed": row['readed'], + "count": row['count'] + }) + + return Response(data) + else: + return JsonResponse({'error': 'No year in header'}, safe=False) + else: + return JsonResponse({'error': 'Unauthorized'}, safe=False) else: - return Response("No year header included") + return JsonResponse({'error': 'No authorization token'}, safe=False) + +# --------------------------------------------- +# Get genres of selected year with percentages +# --------------------------------------------- @api_view(['GET']) def countGenres(request): - if request.META.get('HTTP_YEAR'): - - data = [] - df = filterData(getBooksData(), request.META.get('HTTP_YEAR')) - - genres = df.groupby('genre')['genre'].count().reset_index(name="count") - genres = genres.sort_values(by='count', ascending=False) + if(request.headers.get('Authorization')): + isLoggedIn = isAuthorized(request.headers.get('Authorization')); - for index, row in genres.iterrows(): + if(isLoggedIn): + if request.META.get('HTTP_YEAR'): + + data = [] + df = filterData(getBooksData(), request.META.get('HTTP_YEAR')) + + genres = df.groupby('genre')['genre'].count().reset_index(name="count") + genres = genres.sort_values(by='count', ascending=False) - data.append({ - "genre": row['genre'], - "count": int(row['count']) - }) + for index, row in genres.iterrows(): + data.append({ + "genre": row['genre'], + "count": int(row['count']) + }) - return Response(data) + return Response(data) + else: + return JsonResponse({'error': 'No year in header'}, safe=False) + else: + return JsonResponse({'error': 'Unauthorized'}, safe=False) else: - return Response("No year header included") + return JsonResponse({'error': 'No authorization token'}, safe=False) + +# ---------------- +# Get year stats +# ---------------- @api_view(['GET']) def getStats(request): - if request.META.get('HTTP_YEAR'): - data = [] - df = filterData(getBooksData(), request.META.get('HTTP_YEAR')) - df = df.dropna() + if(request.headers.get('Authorization')): + isLoggedIn = isAuthorized(request.headers.get('Authorization')); - statsTotalBooks = df['name'].count() - statsTotalGenres = df['genre'].nunique() + if(isLoggedIn): + if request.META.get('HTTP_YEAR'): + data = [] + df = filterData(getBooksData(), request.META.get('HTTP_YEAR')) + df = df.dropna() + statsTotalBooks = df['name'].count() + statsTotalGenres = df['genre'].nunique() + avgratingsperyear = round((df['rating'].sum() / df['rating'].count()), 0) + data.append({ + 'totalbooks': statsTotalBooks, + 'totalgenres': statsTotalGenres, + 'avgyearrating': avgratingsperyear + }) - data.append({ - 'totalbooks': statsTotalBooks, - 'totalgenres': statsTotalGenres - }) - - - return Response(data[0]) + return Response(data[0]) + else: + return JsonResponse({'error': 'No year in header'}, safe=False) + else: + return JsonResponse({'error': 'Unauthorized'}, safe=False) else: - return Response("No year header included") + return JsonResponse({'error': 'No authorization token'}, safe=False) -@api_view(['GET']) -def books_per_country(request): - if request.META.get('HTTP_YEAR'): - data = [] - df = filterData(getBooksData(), request.META.get('HTTP_YEAR')) - - countries = df.groupby(['country_code', 'country'])['country'].count().reset_index(name="count") - countries = countries.sort_values(by='count', ascending=False) - - for index, row in countries.iterrows(): - - data.append({ - "code": row['country_code'], - "country": row['country'], - "count": int(row['count']) - }) - - return Response(data) - else: - return Response("No year header included") +# ------------------------------------------------ +# Get books of selected year and group by ratings +# ------------------------------------------------ @api_view(['GET']) def avg_ratings_per_month(request): - datayear = request.META.get('HTTP_YEAR') + if(request.headers.get('Authorization')): + isLoggedIn = isAuthorized(request.headers.get('Authorization')); - if datayear: - data = [] + if(isLoggedIn): + if request.META.get('HTTP_YEAR'): + data = [] + df = filterData(getBooksData(), request.META.get('HTTP_YEAR')) + avgratingspermonth = df.groupby('readed')['rating'].mean().reset_index(name="rating") - # Get CSV file with book data - df = filterData(getBooksData(), request.META.get('HTTP_YEAR')) + for index, row in avgratingspermonth.iterrows(): + data.append({ + "date": row['readed'], + "rating": int(row['rating']) + }) - avgratingspermonth = df.groupby('readed')['rating'].mean().reset_index(name="rating") - - for index, row in avgratingspermonth.iterrows(): - - data.append({ - "date": row['readed'], - "rating": int(row['rating']) - }) - - return Response(data) + return Response(data) + else: + return JsonResponse({'error': 'No year in header'}, safe=False) + else: + return JsonResponse({'error': 'Unauthorized'}, safe=False) else: - return Response("No year header included") + return JsonResponse({'error': 'No authorization token'}, safe=False) + + +# ----------------------------- +# Get ratings of selected year +# ----------------------------- @api_view(['GET']) def countRatings(request): - datayear = request.META.get('HTTP_YEAR') + if(request.headers.get('Authorization')): + isLoggedIn = isAuthorized(request.headers.get('Authorization')); - if datayear: - data = [] + if(isLoggedIn): + if request.META.get('HTTP_YEAR'): + data = [] + df = filterData(getBooksData(), request.META.get('HTTP_YEAR')) + countratings = df.groupby('rating')['rating'].count().reset_index(name="count") + countratings = countratings.sort_values(by='rating', ascending=False) - # Get CSV file with book data - df = filterData(getBooksData(), request.META.get('HTTP_YEAR')) + for index, row in countratings.iterrows(): + data.append({ + "rating": int(row['rating']), + "count": int(row['count']) + }) - countratings = df.groupby('rating')['rating'].count().reset_index(name="count") - countratings = countratings.sort_values(by='rating', ascending=False) - - for index, row in countratings.iterrows(): - - data.append({ - "rating": int(row['rating']), - "count": int(row['count']) - }) - - return Response(data) + return Response(data) + else: + return JsonResponse({'error': 'No year in header'}, safe=False) + else: + return JsonResponse({'error': 'Unauthorized'}, safe=False) else: - return Response("No year header included") \ No newline at end of file + return JsonResponse({'error': 'No authorization token'}, safe=False) \ No newline at end of file diff --git a/ras/api/urls.py b/ras/api/urls.py index ca14b22..14719fd 100644 --- a/ras/api/urls.py +++ b/ras/api/urls.py @@ -3,11 +3,12 @@ from django.views.decorators.csrf import csrf_exempt from .views import * from .modules.auth import * from .modules.crud import * +from .modules.challenges import * from .modules.pandas import * urlpatterns = [ path('books/all', getAllBooks), - path('books', getBooksByYear), + # path('books', getBooksByYear), path('books/years', getYears), path('books/stats', getStats), path('books/insert', addBook), @@ -17,6 +18,11 @@ urlpatterns = [ path('books/genres/count', countGenres), path('books/ratings', avg_ratings_per_month), path('books/ratings/count', countRatings), - path('books/countries', books_per_country), + # path('books/countries', books_per_country), path('auth/login', csrf_exempt(login)), + + path('books/challenge', getChallengeOfYear), + path('books/challenges', getAllChallenges), + path('books/challenges/insert', addChallenge), + path('books/challenges//delete', deleteChallenge), ] \ No newline at end of file diff --git a/ras/frontend/src/App.js b/ras/frontend/src/App.js index 3bfc06b..22a3754 100644 --- a/ras/frontend/src/App.js +++ b/ras/frontend/src/App.js @@ -2,6 +2,8 @@ import React, { Component, lazy, Suspense } from "react"; import { Route, Routes, BrowserRouter as Router } from 'react-router-dom'; import { ColorRing } from 'react-loader-spinner' import Login from "./views/login"; +import Books from "./views/bookslist"; +import BooksList from "./views/bookslist"; const Dashboard = lazy(() => import("./views/dashboard")); @@ -25,6 +27,7 @@ function App() { }> : } /> + } /> {/* } /> */} diff --git a/ras/frontend/src/components/Challenge.js b/ras/frontend/src/components/Challenge.js index 3bc9c14..1a9113b 100644 --- a/ras/frontend/src/components/Challenge.js +++ b/ras/frontend/src/components/Challenge.js @@ -1,59 +1,35 @@ -import React, { Component } from 'react'; -import { getChallenge, getStats } from "./Data.js"; +import React, { useEffect, useState } from 'react'; -export default class Challenge extends Component { - constructor(props) { - super(props); - this.state = { - readingYears: [], - challenge: 0 - } +const Challenge = (props) => { + const [challenge, setChallenge] = useState(0); + const [challengePercentage, setChallengePercentage] = useState(0) + + const getData = async () => { + const data = await import("./Data.js"); + const stats = await data.getStats(props.year); + const yearchallenge = await data.getChallenge(props.year); + setChallenge(yearchallenge ? yearchallenge[0].nrofbooks : 0); + setChallengePercentage(Math.round((stats.totalbooks / yearchallenge[0].nrofbooks) * 100, 0)) } - getComponentData() { - var $this = this; + useEffect(() => { + getData(); + }, [props.year]) - getStats(this.props.year).then(data => { - $this.setState({ - totalbooks: data.totalbooks - }) - }); - - getChallenge(this.props.year).then(data => { - this.setState({ - challenge: data && data.length > 0 ? data[0].nrofbooks : 0 - }) - }); - } - - componentDidMount() { - this.getComponentData(); - } - - componentDidUpdate(prevProps, prevState) { - if (prevProps.year !== this.props.year) { - this.getComponentData(); - } - } - - render() { - var challengePercentage = Math.round((this.state.totalbooks / this.state.challenge) * 100, 0) - - return ( - - {this.state.challenge && this.state.challenge !== 0 ? -
- Book Challenge -
-
-
{challengePercentage}%
-
+ return ( + + {challenge && challenge !== 0 ? +
+ Book Challenge ({challenge} boeken) +
+
+
{challengePercentage}%
- - {this.state.totalbooks}van de{this.state.challenge}boeken gelezen
- : ''} - - ) - } -} \ No newline at end of file +
+ : ''} +
+ ) +} + +export default Challenge; \ No newline at end of file diff --git a/ras/frontend/src/components/Charts.js b/ras/frontend/src/components/Charts.js index e583c5a..68a2f5d 100644 --- a/ras/frontend/src/components/Charts.js +++ b/ras/frontend/src/components/Charts.js @@ -87,8 +87,6 @@ export const initChart = (data, ratings, year) => { order: 1 }) - console.log(dataSet); - /* ---------------------------------- Stacked bar chart @@ -181,7 +179,6 @@ export const initChart = (data, ratings, year) => { export const initDoughnut = (data) => { - console.log(data); var labels = []; var counts = []; diff --git a/ras/frontend/src/components/Data.js b/ras/frontend/src/components/Data.js index c2f7b5b..b034884 100644 --- a/ras/frontend/src/components/Data.js +++ b/ras/frontend/src/components/Data.js @@ -3,19 +3,9 @@ import { readCookie } from "../Functions"; export const getAllBooks = () => { return fetch('/api/books/all', { "method": "GET", - }) - .then(response => response.json()) - .then(data => { - return data; - }) -} - -export const getBooksByYear = (year) => { - return fetch('/api/books', { - "method": "GET", "headers": { - "year": year - } + "Authorization": "Bearer " + localStorage.getItem("token") + }, }) .then(response => response.json()) .then(data => { @@ -23,15 +13,28 @@ export const getBooksByYear = (year) => { }) } -export const getAllChallenges = () => { - return fetch('/api/books/challenges', { - "method": "GET", - }) - .then(response => response.json()) - .then(data => { - return data; - }) -} +// export const getBooksByYear = (year) => { +// return fetch('/api/books', { +// "method": "GET", +// "headers": { +// "year": year +// } +// }) +// .then(response => response.json()) +// .then(data => { +// return data; +// }) +// } + +// export const getAllChallenges = () => { +// return fetch('/api/books/challenges', { +// "method": "GET", +// }) +// .then(response => response.json()) +// .then(data => { +// return data; +// }) +// } export const insertChallenge = (data) => { return fetch('/api/books/challenges/insert', { @@ -69,7 +72,8 @@ export const getStats = (year) => { return fetch('/api/books/stats', { "method": "GET", "headers": { - "year": year + "year": year, + "Authorization": "Bearer " + localStorage.getItem("token") } }) .then(response => response.json()) @@ -94,6 +98,9 @@ export const getChallenge = (year) => { export const getReadingYears = () => { return fetch('/api/books/years', { "method": "GET", + "headers": { + "Authorization": "Bearer " + localStorage.getItem("token") + } }) .then(response => response.json()) .then(data => { @@ -101,37 +108,39 @@ export const getReadingYears = () => { }) } -export const getCountries = (year) => { - return fetch('/api/books/countries', { - "method": "GET", - "headers": { - "year": year - } - }) - .then(response => response.json()) - .then(data => { - return data; - }) -} +// export const getCountries = (year) => { +// return fetch('/api/books/countries', { +// "method": "GET", +// "headers": { +// "year": year +// } +// }) +// .then(response => response.json()) +// .then(data => { +// return data; +// }) +// } -export const getShortestLongestBook = (year) => { - return fetch('/api/books/pages/stats', { - "method": "GET", - "headers": { - "year": year - } - }) - .then(response => response.json()) - .then(data => { - return data; - }) -} +// export const getShortestLongestBook = (year) => { +// return fetch('/api/books/pages/stats', { +// "method": "GET", +// "headers": { +// "year": year, +// "Authorization": "Bearer " + localStorage.getItem("token") +// } +// }) +// .then(response => response.json()) +// .then(data => { +// return data; +// }) +// } export const getBooksPerYearPerGenres = (year) => { return fetch('/api/books/genres', { "method": "GET", "headers": { - "year": year + "year": year, + "Authorization": "Bearer " + localStorage.getItem("token") } }) .then(response => response.json()) @@ -144,7 +153,8 @@ export const getGenresCount = (year) => { return fetch('/api/books/genres/count', { "method": "GET", "headers": { - "year": year + "year": year, + "Authorization": "Bearer " + localStorage.getItem("token") } }) .then(response => response.json()) @@ -157,7 +167,8 @@ export const getAvgRatings = (year) => { return fetch('/api/books/ratings', { "method": "GET", "headers": { - "year": year + "year": year, + "Authorization": "Bearer " + localStorage.getItem("token") } }) .then(response => response.json()) @@ -170,7 +181,8 @@ export const getRatingsCount = (year) => { return fetch('/api/books/ratings/count', { "method": "GET", "headers": { - "year": year + "year": year, + "Authorization": "Bearer " + localStorage.getItem("token") } }) .then(response => response.json()) diff --git a/ras/frontend/src/components/DataTables.css b/ras/frontend/src/components/DataTables.css index 17d989c..a9f1091 100644 --- a/ras/frontend/src/components/DataTables.css +++ b/ras/frontend/src/components/DataTables.css @@ -8,7 +8,7 @@ #DataTable td { color: #333; - font-size: 13px; + font-size: 12px; height: 40px; font-weight: 300; vertical-align: middle; @@ -22,7 +22,8 @@ #DataTable{ margin-bottom:0 !important; - box-shadow: 0 2px 0 1px rgb(0 0 0/3%); + -webkit-box-shadow: 0px 1px 1px 1px #eee; + box-shadow: 0px 1px 1px 1px #eee; } #DataTable a { @@ -30,17 +31,22 @@ text-decoration: none; } +.content-manage #DataTable .fa-star { + color: #ffbe0e; +} + .content-manage #DataTable i { color: #ffffff; } .content-manage #DataTable button{ text-align: center; + border-radius: 100%; } .content-manage #DataTable button i{ margin:auto; - font-size: 14px; + font-size: 12px; } #DataTable i { @@ -49,10 +55,10 @@ } #DataTable thead th{ - padding: 20px 20px; + padding: 15px 20px; border-bottom: 1px solid #efefef; font-weight: 600; - font-size: 15px; + font-size: 12px; color:#000000; } @@ -68,11 +74,6 @@ padding:14px; } -#DataTable{ - margin-bottom:0; - box-shadow: none !important; -} - #DataTable .openAlarm .pictogram { /* display: inline-block; padding: 0 0 0 0; @@ -97,7 +98,7 @@ height: 0px; vertical-align: middle; border-top: none; - border-bottom: none; + border-bottom: solid 1px #f8f8f8 !important; } #DataTable .openAlarm td{ diff --git a/ras/frontend/src/components/Ratings.js b/ras/frontend/src/components/Ratings.js index 430e52d..59ef3fe 100644 --- a/ras/frontend/src/components/Ratings.js +++ b/ras/frontend/src/components/Ratings.js @@ -44,7 +44,7 @@ const Ratings = (props) => {
Ratings - +
@@ -53,12 +53,10 @@ const Ratings = (props) => { - {ratings.map((rating) => { + {ratings.map((rating, i) => { var ratingstars = ''; var rating_percentage = rating[1] / totalRatings * 100; - console.log(rating[1], totalRatings); - if (rating[0]) { for (var i = 0; i < rating[0]; i++) { ratingstars += ""; @@ -66,7 +64,7 @@ const Ratings = (props) => { } return( - +
#
diff --git a/ras/frontend/src/components/Sidebar.js b/ras/frontend/src/components/Sidebar.js index 5fdb6d7..a3ca137 100644 --- a/ras/frontend/src/components/Sidebar.js +++ b/ras/frontend/src/components/Sidebar.js @@ -7,15 +7,21 @@ const Sidebar = () => {
    -
  • Dashboard
  • +
  • Dashboard
  • -
  • Boeken
  • +
  • Boeken
  • + +
  • Challenges
  • +
    +
+ +
    -
  • Instellingen
  • +
  • Instellingen
  • -
  • Uitloggen
  • +
  • Uitloggen
) diff --git a/ras/frontend/src/components/Stats.js b/ras/frontend/src/components/Stats.js index ad4a404..92bac3c 100644 --- a/ras/frontend/src/components/Stats.js +++ b/ras/frontend/src/components/Stats.js @@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react'; const BookStats = (props) =>{ const [totalbooks, setTotalbooks] = useState(0); const [totalgenres, setTotalgenres] = useState(0); + const [yearrating, setYearrating] = useState(0); const getData = async () => { const data = await import("./Data.js"); @@ -10,6 +11,7 @@ const BookStats = (props) =>{ setTotalbooks(stats.totalbooks); setTotalgenres(stats.totalgenres); + setYearrating(stats.avgyearrating); } useEffect(() => { @@ -21,7 +23,7 @@ const BookStats = (props) =>{
- + Gelezen boeken: {totalbooks} @@ -30,7 +32,7 @@ const BookStats = (props) =>{
- + Genres: {totalgenres} @@ -39,9 +41,9 @@ const BookStats = (props) =>{
- + Jaarbeoordeling: - 7 + {yearrating}
diff --git a/ras/frontend/src/views/bookslist.js b/ras/frontend/src/views/bookslist.js new file mode 100644 index 0000000..2c2dabd --- /dev/null +++ b/ras/frontend/src/views/bookslist.js @@ -0,0 +1,74 @@ +import React, { useEffect, useState } from "react"; +import Sidebar from "../components/Sidebar.js"; +import * as moment from 'moment'; +import "../components/DataTables.css"; +moment.locale('nl'); + +const BooksList = (props) => { + var [books, setBooks] = useState([]); + + const getData = async() => { + const data = await import("../components/Data.js"); + const getbooks = await data.getAllBooks(); + console.log(getbooks) + setBooks(getbooks); + + setTimeout(() => { + $('#DataTable').DataTable({ + language: { + url: 'https://cdn.datatables.net/plug-ins/9dcbecd42ad/i18n/Dutch.json', + search: "", + searchPlaceholder: "Zoeken" + }, + dom: 'frt<"bottom"pl><"clear">', + order: [] + }); + }, 300) + } + + useEffect(() => { + getData(); + }, []) + + return ( + + +
+ {/* */} +

Boeken beheren

+ + +
+ + + + + + + + + + + + {books.map((book, i) => { + return ( + + + + + + + + ) + })} + +
BoekSchrijverGelezen opRatingActies
{book.name}{book.author}{moment(book.readed).format('MMMM YYYY')} {book.rating} + +
+
+
+
+ ) +} + +export default BooksList; \ No newline at end of file diff --git a/ras/frontend/src/views/dashboard.js b/ras/frontend/src/views/dashboard.js index a65b444..326dee9 100644 --- a/ras/frontend/src/views/dashboard.js +++ b/ras/frontend/src/views/dashboard.js @@ -3,6 +3,7 @@ import Genres from "../components/Genres"; import Books from "../components/Books"; import Ratings from "../components/Ratings"; import Stats from "../components/Stats"; +import Challenge from "../components/Challenge"; import Sidebar from "../components/Sidebar"; const Dashboard = (props) => { @@ -37,9 +38,15 @@ const Dashboard = (props) => {
+ {/*
+
+ +
+
*/}
+
diff --git a/ras/frontend/static/css/style.css b/ras/frontend/static/css/style.css index 09411bb..ab2204c 100644 --- a/ras/frontend/static/css/style.css +++ b/ras/frontend/static/css/style.css @@ -8,20 +8,14 @@ html, body{ font-family: 'Poppins', sans-serif; letter-spacing: .3px; font-weight: 400; + font-size: 12px; color:#333; } h1{ - /* font-size: 30px; - font-weight: 500; - padding: 0 10px 0px 10px; - margin-bottom: 5px; */ - border-bottom: 1px solid #ddd; display: inline-block; - font-size: 25px; + font-size: 18px; font-weight: 500; - margin-bottom: 20px; - padding: 0 0 10px; width: 100%; } @@ -228,6 +222,13 @@ html, body{ color: #48a5a9; } +.sidebar .bottom-menu{ + position: absolute; + bottom: 0; + width:100%; + border-top: solid 1px rgba(255,255,255,0.2); +} + .books-stats{ margin:20px 0; } @@ -357,7 +358,7 @@ html, body{ .table td{ color: #101010; border-bottom:none !important; - padding: 10px 10px !important; + padding: 10px 20px !important; font-size: 13px; font-weight: 300; } @@ -375,7 +376,13 @@ html, body{ display:none; } - + .challenge .stats-number, .challenge .stats-label{ + display: inline-block; + } + + .challenge .stats-number{ + font-size:14px; + } span.block_name{ color: #333; @@ -592,4 +599,13 @@ html, body{ .stats{ margin-bottom:20px; +} + +.ratingstable thead{ + display:none; +} + +.ratingstable td{ + height: 35px; + padding: 0 10px 0 0px; } \ No newline at end of file diff --git a/ras/frontend/static/js/main.js b/ras/frontend/static/js/main.js index b79f7b0..2169810 100644 --- a/ras/frontend/static/js/main.js +++ b/ras/frontend/static/js/main.js @@ -1,2 +1,2 @@ /*! For license information please see main.js.LICENSE.txt */ -(()=>{var __webpack_modules__={"./node_modules/@emotion/is-prop-valid/dist/emotion-is-prop-valid.esm.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{"use strict";eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _emotion_memoize__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @emotion/memoize */ "./node_modules/@emotion/memoize/dist/emotion-memoize.esm.js");\n\n\nvar reactPropsRegex = /^((children|dangerouslySetInnerHTML|key|ref|autoFocus|defaultValue|defaultChecked|innerHTML|suppressContentEditableWarning|suppressHydrationWarning|valueLink|abbr|accept|acceptCharset|accessKey|action|allow|allowUserMedia|allowPaymentRequest|allowFullScreen|allowTransparency|alt|async|autoComplete|autoPlay|capture|cellPadding|cellSpacing|challenge|charSet|checked|cite|classID|className|cols|colSpan|content|contentEditable|contextMenu|controls|controlsList|coords|crossOrigin|data|dateTime|decoding|default|defer|dir|disabled|disablePictureInPicture|download|draggable|encType|enterKeyHint|form|formAction|formEncType|formMethod|formNoValidate|formTarget|frameBorder|headers|height|hidden|high|href|hrefLang|htmlFor|httpEquiv|id|inputMode|integrity|is|keyParams|keyType|kind|label|lang|list|loading|loop|low|marginHeight|marginWidth|max|maxLength|media|mediaGroup|method|min|minLength|multiple|muted|name|nonce|noValidate|open|optimum|pattern|placeholder|playsInline|poster|preload|profile|radioGroup|readOnly|referrerPolicy|rel|required|reversed|role|rows|rowSpan|sandbox|scope|scoped|scrolling|seamless|selected|shape|size|sizes|slot|span|spellCheck|src|srcDoc|srcLang|srcSet|start|step|style|summary|tabIndex|target|title|translate|type|useMap|value|width|wmode|wrap|about|datatype|inlist|prefix|property|resource|typeof|vocab|autoCapitalize|autoCorrect|autoSave|color|incremental|fallback|inert|itemProp|itemScope|itemType|itemID|itemRef|on|option|results|security|unselectable|accentHeight|accumulate|additive|alignmentBaseline|allowReorder|alphabetic|amplitude|arabicForm|ascent|attributeName|attributeType|autoReverse|azimuth|baseFrequency|baselineShift|baseProfile|bbox|begin|bias|by|calcMode|capHeight|clip|clipPathUnits|clipPath|clipRule|colorInterpolation|colorInterpolationFilters|colorProfile|colorRendering|contentScriptType|contentStyleType|cursor|cx|cy|d|decelerate|descent|diffuseConstant|direction|display|divisor|dominantBaseline|dur|dx|dy|edgeMode|elevation|enableBackground|end|exponent|externalResourcesRequired|fill|fillOpacity|fillRule|filter|filterRes|filterUnits|floodColor|floodOpacity|focusable|fontFamily|fontSize|fontSizeAdjust|fontStretch|fontStyle|fontVariant|fontWeight|format|from|fr|fx|fy|g1|g2|glyphName|glyphOrientationHorizontal|glyphOrientationVertical|glyphRef|gradientTransform|gradientUnits|hanging|horizAdvX|horizOriginX|ideographic|imageRendering|in|in2|intercept|k|k1|k2|k3|k4|kernelMatrix|kernelUnitLength|kerning|keyPoints|keySplines|keyTimes|lengthAdjust|letterSpacing|lightingColor|limitingConeAngle|local|markerEnd|markerMid|markerStart|markerHeight|markerUnits|markerWidth|mask|maskContentUnits|maskUnits|mathematical|mode|numOctaves|offset|opacity|operator|order|orient|orientation|origin|overflow|overlinePosition|overlineThickness|panose1|paintOrder|pathLength|patternContentUnits|patternTransform|patternUnits|pointerEvents|points|pointsAtX|pointsAtY|pointsAtZ|preserveAlpha|preserveAspectRatio|primitiveUnits|r|radius|refX|refY|renderingIntent|repeatCount|repeatDur|requiredExtensions|requiredFeatures|restart|result|rotate|rx|ry|scale|seed|shapeRendering|slope|spacing|specularConstant|specularExponent|speed|spreadMethod|startOffset|stdDeviation|stemh|stemv|stitchTiles|stopColor|stopOpacity|strikethroughPosition|strikethroughThickness|string|stroke|strokeDasharray|strokeDashoffset|strokeLinecap|strokeLinejoin|strokeMiterlimit|strokeOpacity|strokeWidth|surfaceScale|systemLanguage|tableValues|targetX|targetY|textAnchor|textDecoration|textRendering|textLength|to|transform|u1|u2|underlinePosition|underlineThickness|unicode|unicodeBidi|unicodeRange|unitsPerEm|vAlphabetic|vHanging|vIdeographic|vMathematical|values|vectorEffect|version|vertAdvY|vertOriginX|vertOriginY|viewBox|viewTarget|visibility|widths|wordSpacing|writingMode|x|xHeight|x1|x2|xChannelSelector|xlinkActuate|xlinkArcrole|xlinkHref|xlinkRole|xlinkShow|xlinkTitle|xlinkType|xmlBase|xmlns|xmlnsXlink|xmlLang|xmlSpace|y|y1|y2|yChannelSelector|z|zoomAndPan|for|class|autofocus)|(([Dd][Aa][Tt][Aa]|[Aa][Rr][Ii][Aa]|x)-.*))$/; // https://esbench.com/bench/5bfee68a4cd7e6009ef61d23\n\nvar isPropValid = /* #__PURE__ */(0,_emotion_memoize__WEBPACK_IMPORTED_MODULE_0__["default"])(function (prop) {\n return reactPropsRegex.test(prop) || prop.charCodeAt(0) === 111\n /* o */\n && prop.charCodeAt(1) === 110\n /* n */\n && prop.charCodeAt(2) < 91;\n}\n/* Z+1 */\n);\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (isPropValid);\n\n\n//# sourceURL=webpack://frontend/./node_modules/@emotion/is-prop-valid/dist/emotion-is-prop-valid.esm.js?')},"./node_modules/@emotion/memoize/dist/emotion-memoize.esm.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{"use strict";eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\nfunction memoize(fn) {\n var cache = Object.create(null);\n return function (arg) {\n if (cache[arg] === undefined) cache[arg] = fn(arg);\n return cache[arg];\n };\n}\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (memoize);\n\n\n//# sourceURL=webpack://frontend/./node_modules/@emotion/memoize/dist/emotion-memoize.esm.js?')},"./node_modules/@emotion/stylis/dist/stylis.browser.esm.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{"use strict";eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\nfunction stylis_min (W) {\n function M(d, c, e, h, a) {\n for (var m = 0, b = 0, v = 0, n = 0, q, g, x = 0, K = 0, k, u = k = q = 0, l = 0, r = 0, I = 0, t = 0, B = e.length, J = B - 1, y, f = '', p = '', F = '', G = '', C; l < B;) {\n g = e.charCodeAt(l);\n l === J && 0 !== b + n + v + m && (0 !== b && (g = 47 === b ? 10 : 47), n = v = m = 0, B++, J++);\n\n if (0 === b + n + v + m) {\n if (l === J && (0 < r && (f = f.replace(N, '')), 0 < f.trim().length)) {\n switch (g) {\n case 32:\n case 9:\n case 59:\n case 13:\n case 10:\n break;\n\n default:\n f += e.charAt(l);\n }\n\n g = 59;\n }\n\n switch (g) {\n case 123:\n f = f.trim();\n q = f.charCodeAt(0);\n k = 1;\n\n for (t = ++l; l < B;) {\n switch (g = e.charCodeAt(l)) {\n case 123:\n k++;\n break;\n\n case 125:\n k--;\n break;\n\n case 47:\n switch (g = e.charCodeAt(l + 1)) {\n case 42:\n case 47:\n a: {\n for (u = l + 1; u < J; ++u) {\n switch (e.charCodeAt(u)) {\n case 47:\n if (42 === g && 42 === e.charCodeAt(u - 1) && l + 2 !== u) {\n l = u + 1;\n break a;\n }\n\n break;\n\n case 10:\n if (47 === g) {\n l = u + 1;\n break a;\n }\n\n }\n }\n\n l = u;\n }\n\n }\n\n break;\n\n case 91:\n g++;\n\n case 40:\n g++;\n\n case 34:\n case 39:\n for (; l++ < J && e.charCodeAt(l) !== g;) {\n }\n\n }\n\n if (0 === k) break;\n l++;\n }\n\n k = e.substring(t, l);\n 0 === q && (q = (f = f.replace(ca, '').trim()).charCodeAt(0));\n\n switch (q) {\n case 64:\n 0 < r && (f = f.replace(N, ''));\n g = f.charCodeAt(1);\n\n switch (g) {\n case 100:\n case 109:\n case 115:\n case 45:\n r = c;\n break;\n\n default:\n r = O;\n }\n\n k = M(c, r, k, g, a + 1);\n t = k.length;\n 0 < A && (r = X(O, f, I), C = H(3, k, r, c, D, z, t, g, a, h), f = r.join(''), void 0 !== C && 0 === (t = (k = C.trim()).length) && (g = 0, k = ''));\n if (0 < t) switch (g) {\n case 115:\n f = f.replace(da, ea);\n\n case 100:\n case 109:\n case 45:\n k = f + '{' + k + '}';\n break;\n\n case 107:\n f = f.replace(fa, '$1 $2');\n k = f + '{' + k + '}';\n k = 1 === w || 2 === w && L('@' + k, 3) ? '@-webkit-' + k + '@' + k : '@' + k;\n break;\n\n default:\n k = f + k, 112 === h && (k = (p += k, ''));\n } else k = '';\n break;\n\n default:\n k = M(c, X(c, f, I), k, h, a + 1);\n }\n\n F += k;\n k = I = r = u = q = 0;\n f = '';\n g = e.charCodeAt(++l);\n break;\n\n case 125:\n case 59:\n f = (0 < r ? f.replace(N, '') : f).trim();\n if (1 < (t = f.length)) switch (0 === u && (q = f.charCodeAt(0), 45 === q || 96 < q && 123 > q) && (t = (f = f.replace(' ', ':')).length), 0 < A && void 0 !== (C = H(1, f, c, d, D, z, p.length, h, a, h)) && 0 === (t = (f = C.trim()).length) && (f = '\\x00\\x00'), q = f.charCodeAt(0), g = f.charCodeAt(1), q) {\n case 0:\n break;\n\n case 64:\n if (105 === g || 99 === g) {\n G += f + e.charAt(l);\n break;\n }\n\n default:\n 58 !== f.charCodeAt(t - 1) && (p += P(f, q, g, f.charCodeAt(2)));\n }\n I = r = u = q = 0;\n f = '';\n g = e.charCodeAt(++l);\n }\n }\n\n switch (g) {\n case 13:\n case 10:\n 47 === b ? b = 0 : 0 === 1 + q && 107 !== h && 0 < f.length && (r = 1, f += '\\x00');\n 0 < A * Y && H(0, f, c, d, D, z, p.length, h, a, h);\n z = 1;\n D++;\n break;\n\n case 59:\n case 125:\n if (0 === b + n + v + m) {\n z++;\n break;\n }\n\n default:\n z++;\n y = e.charAt(l);\n\n switch (g) {\n case 9:\n case 32:\n if (0 === n + m + b) switch (x) {\n case 44:\n case 58:\n case 9:\n case 32:\n y = '';\n break;\n\n default:\n 32 !== g && (y = ' ');\n }\n break;\n\n case 0:\n y = '\\\\0';\n break;\n\n case 12:\n y = '\\\\f';\n break;\n\n case 11:\n y = '\\\\v';\n break;\n\n case 38:\n 0 === n + b + m && (r = I = 1, y = '\\f' + y);\n break;\n\n case 108:\n if (0 === n + b + m + E && 0 < u) switch (l - u) {\n case 2:\n 112 === x && 58 === e.charCodeAt(l - 3) && (E = x);\n\n case 8:\n 111 === K && (E = K);\n }\n break;\n\n case 58:\n 0 === n + b + m && (u = l);\n break;\n\n case 44:\n 0 === b + v + n + m && (r = 1, y += '\\r');\n break;\n\n case 34:\n case 39:\n 0 === b && (n = n === g ? 0 : 0 === n ? g : n);\n break;\n\n case 91:\n 0 === n + b + v && m++;\n break;\n\n case 93:\n 0 === n + b + v && m--;\n break;\n\n case 41:\n 0 === n + b + m && v--;\n break;\n\n case 40:\n if (0 === n + b + m) {\n if (0 === q) switch (2 * x + 3 * K) {\n case 533:\n break;\n\n default:\n q = 1;\n }\n v++;\n }\n\n break;\n\n case 64:\n 0 === b + v + n + m + u + k && (k = 1);\n break;\n\n case 42:\n case 47:\n if (!(0 < n + m + v)) switch (b) {\n case 0:\n switch (2 * g + 3 * e.charCodeAt(l + 1)) {\n case 235:\n b = 47;\n break;\n\n case 220:\n t = l, b = 42;\n }\n\n break;\n\n case 42:\n 47 === g && 42 === x && t + 2 !== l && (33 === e.charCodeAt(t + 2) && (p += e.substring(t, l + 1)), y = '', b = 0);\n }\n }\n\n 0 === b && (f += y);\n }\n\n K = x;\n x = g;\n l++;\n }\n\n t = p.length;\n\n if (0 < t) {\n r = c;\n if (0 < A && (C = H(2, p, r, d, D, z, t, h, a, h), void 0 !== C && 0 === (p = C).length)) return G + p + F;\n p = r.join(',') + '{' + p + '}';\n\n if (0 !== w * E) {\n 2 !== w || L(p, 2) || (E = 0);\n\n switch (E) {\n case 111:\n p = p.replace(ha, ':-moz-$1') + p;\n break;\n\n case 112:\n p = p.replace(Q, '::-webkit-input-$1') + p.replace(Q, '::-moz-$1') + p.replace(Q, ':-ms-input-$1') + p;\n }\n\n E = 0;\n }\n }\n\n return G + p + F;\n }\n\n function X(d, c, e) {\n var h = c.trim().split(ia);\n c = h;\n var a = h.length,\n m = d.length;\n\n switch (m) {\n case 0:\n case 1:\n var b = 0;\n\n for (d = 0 === m ? '' : d[0] + ' '; b < a; ++b) {\n c[b] = Z(d, c[b], e).trim();\n }\n\n break;\n\n default:\n var v = b = 0;\n\n for (c = []; b < a; ++b) {\n for (var n = 0; n < m; ++n) {\n c[v++] = Z(d[n] + ' ', h[b], e).trim();\n }\n }\n\n }\n\n return c;\n }\n\n function Z(d, c, e) {\n var h = c.charCodeAt(0);\n 33 > h && (h = (c = c.trim()).charCodeAt(0));\n\n switch (h) {\n case 38:\n return c.replace(F, '$1' + d.trim());\n\n case 58:\n return d.trim() + c.replace(F, '$1' + d.trim());\n\n default:\n if (0 < 1 * e && 0 < c.indexOf('\\f')) return c.replace(F, (58 === d.charCodeAt(0) ? '' : '$1') + d.trim());\n }\n\n return d + c;\n }\n\n function P(d, c, e, h) {\n var a = d + ';',\n m = 2 * c + 3 * e + 4 * h;\n\n if (944 === m) {\n d = a.indexOf(':', 9) + 1;\n var b = a.substring(d, a.length - 1).trim();\n b = a.substring(0, d).trim() + b + ';';\n return 1 === w || 2 === w && L(b, 1) ? '-webkit-' + b + b : b;\n }\n\n if (0 === w || 2 === w && !L(a, 1)) return a;\n\n switch (m) {\n case 1015:\n return 97 === a.charCodeAt(10) ? '-webkit-' + a + a : a;\n\n case 951:\n return 116 === a.charCodeAt(3) ? '-webkit-' + a + a : a;\n\n case 963:\n return 110 === a.charCodeAt(5) ? '-webkit-' + a + a : a;\n\n case 1009:\n if (100 !== a.charCodeAt(4)) break;\n\n case 969:\n case 942:\n return '-webkit-' + a + a;\n\n case 978:\n return '-webkit-' + a + '-moz-' + a + a;\n\n case 1019:\n case 983:\n return '-webkit-' + a + '-moz-' + a + '-ms-' + a + a;\n\n case 883:\n if (45 === a.charCodeAt(8)) return '-webkit-' + a + a;\n if (0 < a.indexOf('image-set(', 11)) return a.replace(ja, '$1-webkit-$2') + a;\n break;\n\n case 932:\n if (45 === a.charCodeAt(4)) switch (a.charCodeAt(5)) {\n case 103:\n return '-webkit-box-' + a.replace('-grow', '') + '-webkit-' + a + '-ms-' + a.replace('grow', 'positive') + a;\n\n case 115:\n return '-webkit-' + a + '-ms-' + a.replace('shrink', 'negative') + a;\n\n case 98:\n return '-webkit-' + a + '-ms-' + a.replace('basis', 'preferred-size') + a;\n }\n return '-webkit-' + a + '-ms-' + a + a;\n\n case 964:\n return '-webkit-' + a + '-ms-flex-' + a + a;\n\n case 1023:\n if (99 !== a.charCodeAt(8)) break;\n b = a.substring(a.indexOf(':', 15)).replace('flex-', '').replace('space-between', 'justify');\n return '-webkit-box-pack' + b + '-webkit-' + a + '-ms-flex-pack' + b + a;\n\n case 1005:\n return ka.test(a) ? a.replace(aa, ':-webkit-') + a.replace(aa, ':-moz-') + a : a;\n\n case 1e3:\n b = a.substring(13).trim();\n c = b.indexOf('-') + 1;\n\n switch (b.charCodeAt(0) + b.charCodeAt(c)) {\n case 226:\n b = a.replace(G, 'tb');\n break;\n\n case 232:\n b = a.replace(G, 'tb-rl');\n break;\n\n case 220:\n b = a.replace(G, 'lr');\n break;\n\n default:\n return a;\n }\n\n return '-webkit-' + a + '-ms-' + b + a;\n\n case 1017:\n if (-1 === a.indexOf('sticky', 9)) break;\n\n case 975:\n c = (a = d).length - 10;\n b = (33 === a.charCodeAt(c) ? a.substring(0, c) : a).substring(d.indexOf(':', 7) + 1).trim();\n\n switch (m = b.charCodeAt(0) + (b.charCodeAt(7) | 0)) {\n case 203:\n if (111 > b.charCodeAt(8)) break;\n\n case 115:\n a = a.replace(b, '-webkit-' + b) + ';' + a;\n break;\n\n case 207:\n case 102:\n a = a.replace(b, '-webkit-' + (102 < m ? 'inline-' : '') + 'box') + ';' + a.replace(b, '-webkit-' + b) + ';' + a.replace(b, '-ms-' + b + 'box') + ';' + a;\n }\n\n return a + ';';\n\n case 938:\n if (45 === a.charCodeAt(5)) switch (a.charCodeAt(6)) {\n case 105:\n return b = a.replace('-items', ''), '-webkit-' + a + '-webkit-box-' + b + '-ms-flex-' + b + a;\n\n case 115:\n return '-webkit-' + a + '-ms-flex-item-' + a.replace(ba, '') + a;\n\n default:\n return '-webkit-' + a + '-ms-flex-line-pack' + a.replace('align-content', '').replace(ba, '') + a;\n }\n break;\n\n case 973:\n case 989:\n if (45 !== a.charCodeAt(3) || 122 === a.charCodeAt(4)) break;\n\n case 931:\n case 953:\n if (!0 === la.test(d)) return 115 === (b = d.substring(d.indexOf(':') + 1)).charCodeAt(0) ? P(d.replace('stretch', 'fill-available'), c, e, h).replace(':fill-available', ':stretch') : a.replace(b, '-webkit-' + b) + a.replace(b, '-moz-' + b.replace('fill-', '')) + a;\n break;\n\n case 962:\n if (a = '-webkit-' + a + (102 === a.charCodeAt(5) ? '-ms-' + a : '') + a, 211 === e + h && 105 === a.charCodeAt(13) && 0 < a.indexOf('transform', 10)) return a.substring(0, a.indexOf(';', 27) + 1).replace(ma, '$1-webkit-$2') + a;\n }\n\n return a;\n }\n\n function L(d, c) {\n var e = d.indexOf(1 === c ? ':' : '{'),\n h = d.substring(0, 3 !== c ? e : 10);\n e = d.substring(e + 1, d.length - 1);\n return R(2 !== c ? h : h.replace(na, '$1'), e, c);\n }\n\n function ea(d, c) {\n var e = P(c, c.charCodeAt(0), c.charCodeAt(1), c.charCodeAt(2));\n return e !== c + ';' ? e.replace(oa, ' or ($1)').substring(4) : '(' + c + ')';\n }\n\n function H(d, c, e, h, a, m, b, v, n, q) {\n for (var g = 0, x = c, w; g < A; ++g) {\n switch (w = S[g].call(B, d, x, e, h, a, m, b, v, n, q)) {\n case void 0:\n case !1:\n case !0:\n case null:\n break;\n\n default:\n x = w;\n }\n }\n\n if (x !== c) return x;\n }\n\n function T(d) {\n switch (d) {\n case void 0:\n case null:\n A = S.length = 0;\n break;\n\n default:\n if ('function' === typeof d) S[A++] = d;else if ('object' === typeof d) for (var c = 0, e = d.length; c < e; ++c) {\n T(d[c]);\n } else Y = !!d | 0;\n }\n\n return T;\n }\n\n function U(d) {\n d = d.prefix;\n void 0 !== d && (R = null, d ? 'function' !== typeof d ? w = 1 : (w = 2, R = d) : w = 0);\n return U;\n }\n\n function B(d, c) {\n var e = d;\n 33 > e.charCodeAt(0) && (e = e.trim());\n V = e;\n e = [V];\n\n if (0 < A) {\n var h = H(-1, c, e, e, D, z, 0, 0, 0, 0);\n void 0 !== h && 'string' === typeof h && (c = h);\n }\n\n var a = M(O, e, c, 0, 0);\n 0 < A && (h = H(-2, a, e, e, D, z, a.length, 0, 0, 0), void 0 !== h && (a = h));\n V = '';\n E = 0;\n z = D = 1;\n return a;\n }\n\n var ca = /^\\0+/g,\n N = /[\\0\\r\\f]/g,\n aa = /: */g,\n ka = /zoo|gra/,\n ma = /([,: ])(transform)/g,\n ia = /,\\r+?/g,\n F = /([\\t\\r\\n ])*\\f?&/g,\n fa = /@(k\\w+)\\s*(\\S*)\\s*/,\n Q = /::(place)/g,\n ha = /:(read-only)/g,\n G = /[svh]\\w+-[tblr]{2}/,\n da = /\\(\\s*(.*)\\s*\\)/g,\n oa = /([\\s\\S]*?);/g,\n ba = /-self|flex-/g,\n na = /[^]*?(:[rp][el]a[\\w-]+)[^]*/,\n la = /stretch|:\\s*\\w+\\-(?:conte|avail)/,\n ja = /([^-])(image-set\\()/,\n z = 1,\n D = 1,\n E = 0,\n w = 1,\n O = [],\n S = [],\n A = 0,\n R = null,\n Y = 0,\n V = '';\n B.use = T;\n B.set = U;\n void 0 !== W && U(W);\n return B;\n}\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (stylis_min);\n\n\n//# sourceURL=webpack://frontend/./node_modules/@emotion/stylis/dist/stylis.browser.esm.js?")},"./node_modules/@remix-run/router/dist/router.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{"use strict";eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "AbortedDeferredError": () => (/* binding */ AbortedDeferredError),\n/* harmony export */ "Action": () => (/* binding */ Action),\n/* harmony export */ "ErrorResponse": () => (/* binding */ ErrorResponse),\n/* harmony export */ "IDLE_BLOCKER": () => (/* binding */ IDLE_BLOCKER),\n/* harmony export */ "IDLE_FETCHER": () => (/* binding */ IDLE_FETCHER),\n/* harmony export */ "IDLE_NAVIGATION": () => (/* binding */ IDLE_NAVIGATION),\n/* harmony export */ "UNSAFE_DEFERRED_SYMBOL": () => (/* binding */ UNSAFE_DEFERRED_SYMBOL),\n/* harmony export */ "UNSAFE_DeferredData": () => (/* binding */ DeferredData),\n/* harmony export */ "UNSAFE_convertRoutesToDataRoutes": () => (/* binding */ convertRoutesToDataRoutes),\n/* harmony export */ "UNSAFE_getPathContributingMatches": () => (/* binding */ getPathContributingMatches),\n/* harmony export */ "UNSAFE_invariant": () => (/* binding */ invariant),\n/* harmony export */ "createBrowserHistory": () => (/* binding */ createBrowserHistory),\n/* harmony export */ "createHashHistory": () => (/* binding */ createHashHistory),\n/* harmony export */ "createMemoryHistory": () => (/* binding */ createMemoryHistory),\n/* harmony export */ "createPath": () => (/* binding */ createPath),\n/* harmony export */ "createRouter": () => (/* binding */ createRouter),\n/* harmony export */ "createStaticHandler": () => (/* binding */ createStaticHandler),\n/* harmony export */ "defer": () => (/* binding */ defer),\n/* harmony export */ "generatePath": () => (/* binding */ generatePath),\n/* harmony export */ "getStaticContextFromError": () => (/* binding */ getStaticContextFromError),\n/* harmony export */ "getToPathname": () => (/* binding */ getToPathname),\n/* harmony export */ "isRouteErrorResponse": () => (/* binding */ isRouteErrorResponse),\n/* harmony export */ "joinPaths": () => (/* binding */ joinPaths),\n/* harmony export */ "json": () => (/* binding */ json),\n/* harmony export */ "matchPath": () => (/* binding */ matchPath),\n/* harmony export */ "matchRoutes": () => (/* binding */ matchRoutes),\n/* harmony export */ "normalizePathname": () => (/* binding */ normalizePathname),\n/* harmony export */ "parsePath": () => (/* binding */ parsePath),\n/* harmony export */ "redirect": () => (/* binding */ redirect),\n/* harmony export */ "resolvePath": () => (/* binding */ resolvePath),\n/* harmony export */ "resolveTo": () => (/* binding */ resolveTo),\n/* harmony export */ "stripBasename": () => (/* binding */ stripBasename),\n/* harmony export */ "warning": () => (/* binding */ warning)\n/* harmony export */ });\n/**\n * @remix-run/router v1.3.3\n *\n * Copyright (c) Remix Software Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE.md file in the root directory of this source tree.\n *\n * @license MIT\n */\nfunction _extends() {\n _extends = Object.assign ? Object.assign.bind() : function (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n\n for (var key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n\n return target;\n };\n return _extends.apply(this, arguments);\n}\n\n////////////////////////////////////////////////////////////////////////////////\n//#region Types and Constants\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * Actions represent the type of change to a location value.\n */\nvar Action;\n\n(function (Action) {\n /**\n * A POP indicates a change to an arbitrary index in the history stack, such\n * as a back or forward navigation. It does not describe the direction of the\n * navigation, only that the current index changed.\n *\n * Note: This is the default action for newly created history objects.\n */\n Action["Pop"] = "POP";\n /**\n * A PUSH indicates a new entry being added to the history stack, such as when\n * a link is clicked and a new page loads. When this happens, all subsequent\n * entries in the stack are lost.\n */\n\n Action["Push"] = "PUSH";\n /**\n * A REPLACE indicates the entry at the current index in the history stack\n * being replaced by a new one.\n */\n\n Action["Replace"] = "REPLACE";\n})(Action || (Action = {}));\n\nconst PopStateEventType = "popstate";\n/**\n * Memory history stores the current location in memory. It is designed for use\n * in stateful non-browser environments like tests and React Native.\n */\n\nfunction createMemoryHistory(options) {\n if (options === void 0) {\n options = {};\n }\n\n let {\n initialEntries = ["/"],\n initialIndex,\n v5Compat = false\n } = options;\n let entries; // Declare so we can access from createMemoryLocation\n\n entries = initialEntries.map((entry, index) => createMemoryLocation(entry, typeof entry === "string" ? null : entry.state, index === 0 ? "default" : undefined));\n let index = clampIndex(initialIndex == null ? entries.length - 1 : initialIndex);\n let action = Action.Pop;\n let listener = null;\n\n function clampIndex(n) {\n return Math.min(Math.max(n, 0), entries.length - 1);\n }\n\n function getCurrentLocation() {\n return entries[index];\n }\n\n function createMemoryLocation(to, state, key) {\n if (state === void 0) {\n state = null;\n }\n\n let location = createLocation(entries ? getCurrentLocation().pathname : "/", to, state, key);\n warning$1(location.pathname.charAt(0) === "/", "relative pathnames are not supported in memory history: " + JSON.stringify(to));\n return location;\n }\n\n function createHref(to) {\n return typeof to === "string" ? to : createPath(to);\n }\n\n let history = {\n get index() {\n return index;\n },\n\n get action() {\n return action;\n },\n\n get location() {\n return getCurrentLocation();\n },\n\n createHref,\n\n createURL(to) {\n return new URL(createHref(to), "http://localhost");\n },\n\n encodeLocation(to) {\n let path = typeof to === "string" ? parsePath(to) : to;\n return {\n pathname: path.pathname || "",\n search: path.search || "",\n hash: path.hash || ""\n };\n },\n\n push(to, state) {\n action = Action.Push;\n let nextLocation = createMemoryLocation(to, state);\n index += 1;\n entries.splice(index, entries.length, nextLocation);\n\n if (v5Compat && listener) {\n listener({\n action,\n location: nextLocation,\n delta: 1\n });\n }\n },\n\n replace(to, state) {\n action = Action.Replace;\n let nextLocation = createMemoryLocation(to, state);\n entries[index] = nextLocation;\n\n if (v5Compat && listener) {\n listener({\n action,\n location: nextLocation,\n delta: 0\n });\n }\n },\n\n go(delta) {\n action = Action.Pop;\n let nextIndex = clampIndex(index + delta);\n let nextLocation = entries[nextIndex];\n index = nextIndex;\n\n if (listener) {\n listener({\n action,\n location: nextLocation,\n delta\n });\n }\n },\n\n listen(fn) {\n listener = fn;\n return () => {\n listener = null;\n };\n }\n\n };\n return history;\n}\n/**\n * Browser history stores the location in regular URLs. This is the standard for\n * most web apps, but it requires some configuration on the server to ensure you\n * serve the same app at multiple URLs.\n *\n * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createbrowserhistory\n */\n\nfunction createBrowserHistory(options) {\n if (options === void 0) {\n options = {};\n }\n\n function createBrowserLocation(window, globalHistory) {\n let {\n pathname,\n search,\n hash\n } = window.location;\n return createLocation("", {\n pathname,\n search,\n hash\n }, // state defaults to `null` because `window.history.state` does\n globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || "default");\n }\n\n function createBrowserHref(window, to) {\n return typeof to === "string" ? to : createPath(to);\n }\n\n return getUrlBasedHistory(createBrowserLocation, createBrowserHref, null, options);\n}\n/**\n * Hash history stores the location in window.location.hash. This makes it ideal\n * for situations where you don\'t want to send the location to the server for\n * some reason, either because you do cannot configure it or the URL space is\n * reserved for something else.\n *\n * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createhashhistory\n */\n\nfunction createHashHistory(options) {\n if (options === void 0) {\n options = {};\n }\n\n function createHashLocation(window, globalHistory) {\n let {\n pathname = "/",\n search = "",\n hash = ""\n } = parsePath(window.location.hash.substr(1));\n return createLocation("", {\n pathname,\n search,\n hash\n }, // state defaults to `null` because `window.history.state` does\n globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || "default");\n }\n\n function createHashHref(window, to) {\n let base = window.document.querySelector("base");\n let href = "";\n\n if (base && base.getAttribute("href")) {\n let url = window.location.href;\n let hashIndex = url.indexOf("#");\n href = hashIndex === -1 ? url : url.slice(0, hashIndex);\n }\n\n return href + "#" + (typeof to === "string" ? to : createPath(to));\n }\n\n function validateHashLocation(location, to) {\n warning$1(location.pathname.charAt(0) === "/", "relative pathnames are not supported in hash history.push(" + JSON.stringify(to) + ")");\n }\n\n return getUrlBasedHistory(createHashLocation, createHashHref, validateHashLocation, options);\n}\nfunction invariant(value, message) {\n if (value === false || value === null || typeof value === "undefined") {\n throw new Error(message);\n }\n}\n\nfunction warning$1(cond, message) {\n if (!cond) {\n // eslint-disable-next-line no-console\n if (typeof console !== "undefined") console.warn(message);\n\n try {\n // Welcome to debugging history!\n //\n // This error is thrown as a convenience so you can more easily\n // find the source for a warning that appears in the console by\n // enabling "pause on exceptions" in your JavaScript debugger.\n throw new Error(message); // eslint-disable-next-line no-empty\n } catch (e) {}\n }\n}\n\nfunction createKey() {\n return Math.random().toString(36).substr(2, 8);\n}\n/**\n * For browser-based histories, we combine the state and key into an object\n */\n\n\nfunction getHistoryState(location, index) {\n return {\n usr: location.state,\n key: location.key,\n idx: index\n };\n}\n/**\n * Creates a Location object with a unique key from the given Path\n */\n\n\nfunction createLocation(current, to, state, key) {\n if (state === void 0) {\n state = null;\n }\n\n let location = _extends({\n pathname: typeof current === "string" ? current : current.pathname,\n search: "",\n hash: ""\n }, typeof to === "string" ? parsePath(to) : to, {\n state,\n // TODO: This could be cleaned up. push/replace should probably just take\n // full Locations now and avoid the need to run through this flow at all\n // But that\'s a pretty big refactor to the current test suite so going to\n // keep as is for the time being and just let any incoming keys take precedence\n key: to && to.key || key || createKey()\n });\n\n return location;\n}\n/**\n * Creates a string URL path from the given pathname, search, and hash components.\n */\n\nfunction createPath(_ref) {\n let {\n pathname = "/",\n search = "",\n hash = ""\n } = _ref;\n if (search && search !== "?") pathname += search.charAt(0) === "?" ? search : "?" + search;\n if (hash && hash !== "#") pathname += hash.charAt(0) === "#" ? hash : "#" + hash;\n return pathname;\n}\n/**\n * Parses a string URL path into its separate pathname, search, and hash components.\n */\n\nfunction parsePath(path) {\n let parsedPath = {};\n\n if (path) {\n let hashIndex = path.indexOf("#");\n\n if (hashIndex >= 0) {\n parsedPath.hash = path.substr(hashIndex);\n path = path.substr(0, hashIndex);\n }\n\n let searchIndex = path.indexOf("?");\n\n if (searchIndex >= 0) {\n parsedPath.search = path.substr(searchIndex);\n path = path.substr(0, searchIndex);\n }\n\n if (path) {\n parsedPath.pathname = path;\n }\n }\n\n return parsedPath;\n}\n\nfunction getUrlBasedHistory(getLocation, createHref, validateLocation, options) {\n if (options === void 0) {\n options = {};\n }\n\n let {\n window = document.defaultView,\n v5Compat = false\n } = options;\n let globalHistory = window.history;\n let action = Action.Pop;\n let listener = null;\n let index = getIndex(); // Index should only be null when we initialize. If not, it\'s because the\n // user called history.pushState or history.replaceState directly, in which\n // case we should log a warning as it will result in bugs.\n\n if (index == null) {\n index = 0;\n globalHistory.replaceState(_extends({}, globalHistory.state, {\n idx: index\n }), "");\n }\n\n function getIndex() {\n let state = globalHistory.state || {\n idx: null\n };\n return state.idx;\n }\n\n function handlePop() {\n action = Action.Pop;\n let nextIndex = getIndex();\n let delta = nextIndex == null ? null : nextIndex - index;\n index = nextIndex;\n\n if (listener) {\n listener({\n action,\n location: history.location,\n delta\n });\n }\n }\n\n function push(to, state) {\n action = Action.Push;\n let location = createLocation(history.location, to, state);\n if (validateLocation) validateLocation(location, to);\n index = getIndex() + 1;\n let historyState = getHistoryState(location, index);\n let url = history.createHref(location); // try...catch because iOS limits us to 100 pushState calls :/\n\n try {\n globalHistory.pushState(historyState, "", url);\n } catch (error) {\n // They are going to lose state here, but there is no real\n // way to warn them about it since the page will refresh...\n window.location.assign(url);\n }\n\n if (v5Compat && listener) {\n listener({\n action,\n location: history.location,\n delta: 1\n });\n }\n }\n\n function replace(to, state) {\n action = Action.Replace;\n let location = createLocation(history.location, to, state);\n if (validateLocation) validateLocation(location, to);\n index = getIndex();\n let historyState = getHistoryState(location, index);\n let url = history.createHref(location);\n globalHistory.replaceState(historyState, "", url);\n\n if (v5Compat && listener) {\n listener({\n action,\n location: history.location,\n delta: 0\n });\n }\n }\n\n function createURL(to) {\n // window.location.origin is "null" (the literal string value) in Firefox\n // under certain conditions, notably when serving from a local HTML file\n // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297\n let base = window.location.origin !== "null" ? window.location.origin : window.location.href;\n let href = typeof to === "string" ? to : createPath(to);\n invariant(base, "No window.location.(origin|href) available to create URL for href: " + href);\n return new URL(href, base);\n }\n\n let history = {\n get action() {\n return action;\n },\n\n get location() {\n return getLocation(window, globalHistory);\n },\n\n listen(fn) {\n if (listener) {\n throw new Error("A history only accepts one active listener");\n }\n\n window.addEventListener(PopStateEventType, handlePop);\n listener = fn;\n return () => {\n window.removeEventListener(PopStateEventType, handlePop);\n listener = null;\n };\n },\n\n createHref(to) {\n return createHref(window, to);\n },\n\n createURL,\n\n encodeLocation(to) {\n // Encode a Location the same way window.location would\n let url = createURL(to);\n return {\n pathname: url.pathname,\n search: url.search,\n hash: url.hash\n };\n },\n\n push,\n replace,\n\n go(n) {\n return globalHistory.go(n);\n }\n\n };\n return history;\n} //#endregion\n\nvar ResultType;\n\n(function (ResultType) {\n ResultType["data"] = "data";\n ResultType["deferred"] = "deferred";\n ResultType["redirect"] = "redirect";\n ResultType["error"] = "error";\n})(ResultType || (ResultType = {}));\n\nfunction isIndexRoute(route) {\n return route.index === true;\n} // Walk the route tree generating unique IDs where necessary so we are working\n// solely with AgnosticDataRouteObject\'s within the Router\n\n\nfunction convertRoutesToDataRoutes(routes, parentPath, allIds) {\n if (parentPath === void 0) {\n parentPath = [];\n }\n\n if (allIds === void 0) {\n allIds = new Set();\n }\n\n return routes.map((route, index) => {\n let treePath = [...parentPath, index];\n let id = typeof route.id === "string" ? route.id : treePath.join("-");\n invariant(route.index !== true || !route.children, "Cannot specify children on an index route");\n invariant(!allIds.has(id), "Found a route id collision on id \\"" + id + "\\". Route " + "id\'s must be globally unique within Data Router usages");\n allIds.add(id);\n\n if (isIndexRoute(route)) {\n let indexRoute = _extends({}, route, {\n id\n });\n\n return indexRoute;\n } else {\n let pathOrLayoutRoute = _extends({}, route, {\n id,\n children: route.children ? convertRoutesToDataRoutes(route.children, treePath, allIds) : undefined\n });\n\n return pathOrLayoutRoute;\n }\n });\n}\n/**\n * Matches the given routes to a location and returns the match data.\n *\n * @see https://reactrouter.com/utils/match-routes\n */\n\nfunction matchRoutes(routes, locationArg, basename) {\n if (basename === void 0) {\n basename = "/";\n }\n\n let location = typeof locationArg === "string" ? parsePath(locationArg) : locationArg;\n let pathname = stripBasename(location.pathname || "/", basename);\n\n if (pathname == null) {\n return null;\n }\n\n let branches = flattenRoutes(routes);\n rankRouteBranches(branches);\n let matches = null;\n\n for (let i = 0; matches == null && i < branches.length; ++i) {\n matches = matchRouteBranch(branches[i], // Incoming pathnames are generally encoded from either window.location\n // or from router.navigate, but we want to match against the unencoded\n // paths in the route definitions. Memory router locations won\'t be\n // encoded here but there also shouldn\'t be anything to decode so this\n // should be a safe operation. This avoids needing matchRoutes to be\n // history-aware.\n safelyDecodeURI(pathname));\n }\n\n return matches;\n}\n\nfunction flattenRoutes(routes, branches, parentsMeta, parentPath) {\n if (branches === void 0) {\n branches = [];\n }\n\n if (parentsMeta === void 0) {\n parentsMeta = [];\n }\n\n if (parentPath === void 0) {\n parentPath = "";\n }\n\n let flattenRoute = (route, index, relativePath) => {\n let meta = {\n relativePath: relativePath === undefined ? route.path || "" : relativePath,\n caseSensitive: route.caseSensitive === true,\n childrenIndex: index,\n route\n };\n\n if (meta.relativePath.startsWith("/")) {\n invariant(meta.relativePath.startsWith(parentPath), "Absolute route path \\"" + meta.relativePath + "\\" nested under path " + ("\\"" + parentPath + "\\" is not valid. An absolute child route path ") + "must start with the combined path of all its parent routes.");\n meta.relativePath = meta.relativePath.slice(parentPath.length);\n }\n\n let path = joinPaths([parentPath, meta.relativePath]);\n let routesMeta = parentsMeta.concat(meta); // Add the children before adding this route to the array so we traverse the\n // route tree depth-first and child routes appear before their parents in\n // the "flattened" version.\n\n if (route.children && route.children.length > 0) {\n invariant( // Our types know better, but runtime JS may not!\n // @ts-expect-error\n route.index !== true, "Index routes must not have child routes. Please remove " + ("all child routes from route path \\"" + path + "\\"."));\n flattenRoutes(route.children, branches, routesMeta, path);\n } // Routes without a path shouldn\'t ever match by themselves unless they are\n // index routes, so don\'t add them to the list of possible branches.\n\n\n if (route.path == null && !route.index) {\n return;\n }\n\n branches.push({\n path,\n score: computeScore(path, route.index),\n routesMeta\n });\n };\n\n routes.forEach((route, index) => {\n var _route$path;\n\n // coarse-grain check for optional params\n if (route.path === "" || !((_route$path = route.path) != null && _route$path.includes("?"))) {\n flattenRoute(route, index);\n } else {\n for (let exploded of explodeOptionalSegments(route.path)) {\n flattenRoute(route, index, exploded);\n }\n }\n });\n return branches;\n}\n/**\n * Computes all combinations of optional path segments for a given path,\n * excluding combinations that are ambiguous and of lower priority.\n *\n * For example, `/one/:two?/three/:four?/:five?` explodes to:\n * - `/one/three`\n * - `/one/:two/three`\n * - `/one/three/:four`\n * - `/one/three/:five`\n * - `/one/:two/three/:four`\n * - `/one/:two/three/:five`\n * - `/one/three/:four/:five`\n * - `/one/:two/three/:four/:five`\n */\n\n\nfunction explodeOptionalSegments(path) {\n let segments = path.split("/");\n if (segments.length === 0) return [];\n let [first, ...rest] = segments; // Optional path segments are denoted by a trailing `?`\n\n let isOptional = first.endsWith("?"); // Compute the corresponding required segment: `foo?` -> `foo`\n\n let required = first.replace(/\\?$/, "");\n\n if (rest.length === 0) {\n // Intepret empty string as omitting an optional segment\n // `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`\n return isOptional ? [required, ""] : [required];\n }\n\n let restExploded = explodeOptionalSegments(rest.join("/"));\n let result = []; // All child paths with the prefix. Do this for all children before the\n // optional version for all children so we get consistent ordering where the\n // parent optional aspect is preferred as required. Otherwise, we can get\n // child sections interspersed where deeper optional segments are higher than\n // parent optional segments, where for example, /:two would explodes _earlier_\n // then /:one. By always including the parent as required _for all children_\n // first, we avoid this issue\n\n result.push(...restExploded.map(subpath => subpath === "" ? required : [required, subpath].join("/"))); // Then if this is an optional value, add all child versions without\n\n if (isOptional) {\n result.push(...restExploded);\n } // for absolute paths, ensure `/` instead of empty segment\n\n\n return result.map(exploded => path.startsWith("/") && exploded === "" ? "/" : exploded);\n}\n\nfunction rankRouteBranches(branches) {\n branches.sort((a, b) => a.score !== b.score ? b.score - a.score // Higher score first\n : compareIndexes(a.routesMeta.map(meta => meta.childrenIndex), b.routesMeta.map(meta => meta.childrenIndex)));\n}\n\nconst paramRe = /^:\\w+$/;\nconst dynamicSegmentValue = 3;\nconst indexRouteValue = 2;\nconst emptySegmentValue = 1;\nconst staticSegmentValue = 10;\nconst splatPenalty = -2;\n\nconst isSplat = s => s === "*";\n\nfunction computeScore(path, index) {\n let segments = path.split("/");\n let initialScore = segments.length;\n\n if (segments.some(isSplat)) {\n initialScore += splatPenalty;\n }\n\n if (index) {\n initialScore += indexRouteValue;\n }\n\n return segments.filter(s => !isSplat(s)).reduce((score, segment) => score + (paramRe.test(segment) ? dynamicSegmentValue : segment === "" ? emptySegmentValue : staticSegmentValue), initialScore);\n}\n\nfunction compareIndexes(a, b) {\n let siblings = a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);\n return siblings ? // If two routes are siblings, we should try to match the earlier sibling\n // first. This allows people to have fine-grained control over the matching\n // behavior by simply putting routes with identical paths in the order they\n // want them tried.\n a[a.length - 1] - b[b.length - 1] : // Otherwise, it doesn\'t really make sense to rank non-siblings by index,\n // so they sort equally.\n 0;\n}\n\nfunction matchRouteBranch(branch, pathname) {\n let {\n routesMeta\n } = branch;\n let matchedParams = {};\n let matchedPathname = "/";\n let matches = [];\n\n for (let i = 0; i < routesMeta.length; ++i) {\n let meta = routesMeta[i];\n let end = i === routesMeta.length - 1;\n let remainingPathname = matchedPathname === "/" ? pathname : pathname.slice(matchedPathname.length) || "/";\n let match = matchPath({\n path: meta.relativePath,\n caseSensitive: meta.caseSensitive,\n end\n }, remainingPathname);\n if (!match) return null;\n Object.assign(matchedParams, match.params);\n let route = meta.route;\n matches.push({\n // TODO: Can this as be avoided?\n params: matchedParams,\n pathname: joinPaths([matchedPathname, match.pathname]),\n pathnameBase: normalizePathname(joinPaths([matchedPathname, match.pathnameBase])),\n route\n });\n\n if (match.pathnameBase !== "/") {\n matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);\n }\n }\n\n return matches;\n}\n/**\n * Returns a path with params interpolated.\n *\n * @see https://reactrouter.com/utils/generate-path\n */\n\n\nfunction generatePath(originalPath, params) {\n if (params === void 0) {\n params = {};\n }\n\n let path = originalPath;\n\n if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {\n warning(false, "Route path \\"" + path + "\\" will be treated as if it were " + ("\\"" + path.replace(/\\*$/, "/*") + "\\" because the `*` character must ") + "always follow a `/` in the pattern. To get rid of this warning, " + ("please change the route path to \\"" + path.replace(/\\*$/, "/*") + "\\"."));\n path = path.replace(/\\*$/, "/*");\n }\n\n return path.replace(/^:(\\w+)(\\??)/g, (_, key, optional) => {\n let param = params[key];\n\n if (optional === "?") {\n return param == null ? "" : param;\n }\n\n if (param == null) {\n invariant(false, "Missing \\":" + key + "\\" param");\n }\n\n return param;\n }).replace(/\\/:(\\w+)(\\??)/g, (_, key, optional) => {\n let param = params[key];\n\n if (optional === "?") {\n return param == null ? "" : "/" + param;\n }\n\n if (param == null) {\n invariant(false, "Missing \\":" + key + "\\" param");\n }\n\n return "/" + param;\n }) // Remove any optional markers from optional static segments\n .replace(/\\?/g, "").replace(/(\\/?)\\*/, (_, prefix, __, str) => {\n const star = "*";\n\n if (params[star] == null) {\n // If no splat was provided, trim the trailing slash _unless_ it\'s\n // the entire path\n return str === "/*" ? "/" : "";\n } // Apply the splat\n\n\n return "" + prefix + params[star];\n });\n}\n/**\n * Performs pattern matching on a URL pathname and returns information about\n * the match.\n *\n * @see https://reactrouter.com/utils/match-path\n */\n\nfunction matchPath(pattern, pathname) {\n if (typeof pattern === "string") {\n pattern = {\n path: pattern,\n caseSensitive: false,\n end: true\n };\n }\n\n let [matcher, paramNames] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);\n let match = pathname.match(matcher);\n if (!match) return null;\n let matchedPathname = match[0];\n let pathnameBase = matchedPathname.replace(/(.)\\/+$/, "$1");\n let captureGroups = match.slice(1);\n let params = paramNames.reduce((memo, paramName, index) => {\n // We need to compute the pathnameBase here using the raw splat value\n // instead of using params["*"] later because it will be decoded then\n if (paramName === "*") {\n let splatValue = captureGroups[index] || "";\n pathnameBase = matchedPathname.slice(0, matchedPathname.length - splatValue.length).replace(/(.)\\/+$/, "$1");\n }\n\n memo[paramName] = safelyDecodeURIComponent(captureGroups[index] || "", paramName);\n return memo;\n }, {});\n return {\n params,\n pathname: matchedPathname,\n pathnameBase,\n pattern\n };\n}\n\nfunction compilePath(path, caseSensitive, end) {\n if (caseSensitive === void 0) {\n caseSensitive = false;\n }\n\n if (end === void 0) {\n end = true;\n }\n\n warning(path === "*" || !path.endsWith("*") || path.endsWith("/*"), "Route path \\"" + path + "\\" will be treated as if it were " + ("\\"" + path.replace(/\\*$/, "/*") + "\\" because the `*` character must ") + "always follow a `/` in the pattern. To get rid of this warning, " + ("please change the route path to \\"" + path.replace(/\\*$/, "/*") + "\\"."));\n let paramNames = [];\n let regexpSource = "^" + path.replace(/\\/*\\*?$/, "") // Ignore trailing / and /*, we\'ll handle it below\n .replace(/^\\/*/, "/") // Make sure it has a leading /\n .replace(/[\\\\.*+^$?{}|()[\\]]/g, "\\\\$&") // Escape special regex chars\n .replace(/\\/:(\\w+)/g, (_, paramName) => {\n paramNames.push(paramName);\n return "/([^\\\\/]+)";\n });\n\n if (path.endsWith("*")) {\n paramNames.push("*");\n regexpSource += path === "*" || path === "/*" ? "(.*)$" // Already matched the initial /, just match the rest\n : "(?:\\\\/(.+)|\\\\/*)$"; // Don\'t include the / in params["*"]\n } else if (end) {\n // When matching to the end, ignore trailing slashes\n regexpSource += "\\\\/*$";\n } else if (path !== "" && path !== "/") {\n // If our path is non-empty and contains anything beyond an initial slash,\n // then we have _some_ form of path in our regex so we should expect to\n // match only if we find the end of this path segment. Look for an optional\n // non-captured trailing slash (to match a portion of the URL) or the end\n // of the path (if we\'ve matched to the end). We used to do this with a\n // word boundary but that gives false positives on routes like\n // /user-preferences since `-` counts as a word boundary.\n regexpSource += "(?:(?=\\\\/|$))";\n } else ;\n\n let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");\n return [matcher, paramNames];\n}\n\nfunction safelyDecodeURI(value) {\n try {\n return decodeURI(value);\n } catch (error) {\n warning(false, "The URL path \\"" + value + "\\" could not be decoded because it is is a " + "malformed URL segment. This is probably due to a bad percent " + ("encoding (" + error + ")."));\n return value;\n }\n}\n\nfunction safelyDecodeURIComponent(value, paramName) {\n try {\n return decodeURIComponent(value);\n } catch (error) {\n warning(false, "The value for the URL param \\"" + paramName + "\\" will not be decoded because" + (" the string \\"" + value + "\\" is a malformed URL segment. This is probably") + (" due to a bad percent encoding (" + error + ")."));\n return value;\n }\n}\n/**\n * @private\n */\n\n\nfunction stripBasename(pathname, basename) {\n if (basename === "/") return pathname;\n\n if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {\n return null;\n } // We want to leave trailing slash behavior in the user\'s control, so if they\n // specify a basename with a trailing slash, we should support it\n\n\n let startIndex = basename.endsWith("/") ? basename.length - 1 : basename.length;\n let nextChar = pathname.charAt(startIndex);\n\n if (nextChar && nextChar !== "/") {\n // pathname does not start with basename/\n return null;\n }\n\n return pathname.slice(startIndex) || "/";\n}\n/**\n * @private\n */\n\nfunction warning(cond, message) {\n if (!cond) {\n // eslint-disable-next-line no-console\n if (typeof console !== "undefined") console.warn(message);\n\n try {\n // Welcome to debugging @remix-run/router!\n //\n // This error is thrown as a convenience so you can more easily\n // find the source for a warning that appears in the console by\n // enabling "pause on exceptions" in your JavaScript debugger.\n throw new Error(message); // eslint-disable-next-line no-empty\n } catch (e) {}\n }\n}\n/**\n * Returns a resolved path object relative to the given pathname.\n *\n * @see https://reactrouter.com/utils/resolve-path\n */\n\nfunction resolvePath(to, fromPathname) {\n if (fromPathname === void 0) {\n fromPathname = "/";\n }\n\n let {\n pathname: toPathname,\n search = "",\n hash = ""\n } = typeof to === "string" ? parsePath(to) : to;\n let pathname = toPathname ? toPathname.startsWith("/") ? toPathname : resolvePathname(toPathname, fromPathname) : fromPathname;\n return {\n pathname,\n search: normalizeSearch(search),\n hash: normalizeHash(hash)\n };\n}\n\nfunction resolvePathname(relativePath, fromPathname) {\n let segments = fromPathname.replace(/\\/+$/, "").split("/");\n let relativeSegments = relativePath.split("/");\n relativeSegments.forEach(segment => {\n if (segment === "..") {\n // Keep the root "" segment so the pathname starts at /\n if (segments.length > 1) segments.pop();\n } else if (segment !== ".") {\n segments.push(segment);\n }\n });\n return segments.length > 1 ? segments.join("/") : "/";\n}\n\nfunction getInvalidPathError(char, field, dest, path) {\n return "Cannot include a \'" + char + "\' character in a manually specified " + ("`to." + field + "` field [" + JSON.stringify(path) + "]. Please separate it out to the ") + ("`to." + dest + "` field. Alternatively you may provide the full path as ") + "a string in and the router will parse it for you.";\n}\n/**\n * @private\n *\n * When processing relative navigation we want to ignore ancestor routes that\n * do not contribute to the path, such that index/pathless layout routes don\'t\n * interfere.\n *\n * For example, when moving a route element into an index route and/or a\n * pathless layout route, relative link behavior contained within should stay\n * the same. Both of the following examples should link back to the root:\n *\n * \n * \n * \n *\n * \n * \n * }> // <-- Does not contribute\n * // <-- Does not contribute\n * \n * \n */\n\n\nfunction getPathContributingMatches(matches) {\n return matches.filter((match, index) => index === 0 || match.route.path && match.route.path.length > 0);\n}\n/**\n * @private\n */\n\nfunction resolveTo(toArg, routePathnames, locationPathname, isPathRelative) {\n if (isPathRelative === void 0) {\n isPathRelative = false;\n }\n\n let to;\n\n if (typeof toArg === "string") {\n to = parsePath(toArg);\n } else {\n to = _extends({}, toArg);\n invariant(!to.pathname || !to.pathname.includes("?"), getInvalidPathError("?", "pathname", "search", to));\n invariant(!to.pathname || !to.pathname.includes("#"), getInvalidPathError("#", "pathname", "hash", to));\n invariant(!to.search || !to.search.includes("#"), getInvalidPathError("#", "search", "hash", to));\n }\n\n let isEmptyPath = toArg === "" || to.pathname === "";\n let toPathname = isEmptyPath ? "/" : to.pathname;\n let from; // Routing is relative to the current pathname if explicitly requested.\n //\n // If a pathname is explicitly provided in `to`, it should be relative to the\n // route context. This is explained in `Note on `` values` in our\n // migration guide from v5 as a means of disambiguation between `to` values\n // that begin with `/` and those that do not. However, this is problematic for\n // `to` values that do not provide a pathname. `to` can simply be a search or\n // hash string, in which case we should assume that the navigation is relative\n // to the current location\'s pathname and *not* the route pathname.\n\n if (isPathRelative || toPathname == null) {\n from = locationPathname;\n } else {\n let routePathnameIndex = routePathnames.length - 1;\n\n if (toPathname.startsWith("..")) {\n let toSegments = toPathname.split("/"); // Each leading .. segment means "go up one route" instead of "go up one\n // URL segment". This is a key difference from how works and a\n // major reason we call this a "to" value instead of a "href".\n\n while (toSegments[0] === "..") {\n toSegments.shift();\n routePathnameIndex -= 1;\n }\n\n to.pathname = toSegments.join("/");\n } // If there are more ".." segments than parent routes, resolve relative to\n // the root / URL.\n\n\n from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";\n }\n\n let path = resolvePath(to, from); // Ensure the pathname has a trailing slash if the original "to" had one\n\n let hasExplicitTrailingSlash = toPathname && toPathname !== "/" && toPathname.endsWith("/"); // Or if this was a link to the current path which has a trailing slash\n\n let hasCurrentTrailingSlash = (isEmptyPath || toPathname === ".") && locationPathname.endsWith("/");\n\n if (!path.pathname.endsWith("/") && (hasExplicitTrailingSlash || hasCurrentTrailingSlash)) {\n path.pathname += "/";\n }\n\n return path;\n}\n/**\n * @private\n */\n\nfunction getToPathname(to) {\n // Empty strings should be treated the same as / paths\n return to === "" || to.pathname === "" ? "/" : typeof to === "string" ? parsePath(to).pathname : to.pathname;\n}\n/**\n * @private\n */\n\nconst joinPaths = paths => paths.join("/").replace(/\\/\\/+/g, "/");\n/**\n * @private\n */\n\nconst normalizePathname = pathname => pathname.replace(/\\/+$/, "").replace(/^\\/*/, "/");\n/**\n * @private\n */\n\nconst normalizeSearch = search => !search || search === "?" ? "" : search.startsWith("?") ? search : "?" + search;\n/**\n * @private\n */\n\nconst normalizeHash = hash => !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash;\n/**\n * This is a shortcut for creating `application/json` responses. Converts `data`\n * to JSON and sets the `Content-Type` header.\n */\n\nconst json = function json(data, init) {\n if (init === void 0) {\n init = {};\n }\n\n let responseInit = typeof init === "number" ? {\n status: init\n } : init;\n let headers = new Headers(responseInit.headers);\n\n if (!headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json; charset=utf-8");\n }\n\n return new Response(JSON.stringify(data), _extends({}, responseInit, {\n headers\n }));\n};\nclass AbortedDeferredError extends Error {}\nclass DeferredData {\n constructor(data, responseInit) {\n this.pendingKeysSet = new Set();\n this.subscribers = new Set();\n this.deferredKeys = [];\n invariant(data && typeof data === "object" && !Array.isArray(data), "defer() only accepts plain objects"); // Set up an AbortController + Promise we can race against to exit early\n // cancellation\n\n let reject;\n this.abortPromise = new Promise((_, r) => reject = r);\n this.controller = new AbortController();\n\n let onAbort = () => reject(new AbortedDeferredError("Deferred data aborted"));\n\n this.unlistenAbortSignal = () => this.controller.signal.removeEventListener("abort", onAbort);\n\n this.controller.signal.addEventListener("abort", onAbort);\n this.data = Object.entries(data).reduce((acc, _ref) => {\n let [key, value] = _ref;\n return Object.assign(acc, {\n [key]: this.trackPromise(key, value)\n });\n }, {});\n\n if (this.done) {\n // All incoming values were resolved\n this.unlistenAbortSignal();\n }\n\n this.init = responseInit;\n }\n\n trackPromise(key, value) {\n if (!(value instanceof Promise)) {\n return value;\n }\n\n this.deferredKeys.push(key);\n this.pendingKeysSet.add(key); // We store a little wrapper promise that will be extended with\n // _data/_error props upon resolve/reject\n\n let promise = Promise.race([value, this.abortPromise]).then(data => this.onSettle(promise, key, null, data), error => this.onSettle(promise, key, error)); // Register rejection listeners to avoid uncaught promise rejections on\n // errors or aborted deferred values\n\n promise.catch(() => {});\n Object.defineProperty(promise, "_tracked", {\n get: () => true\n });\n return promise;\n }\n\n onSettle(promise, key, error, data) {\n if (this.controller.signal.aborted && error instanceof AbortedDeferredError) {\n this.unlistenAbortSignal();\n Object.defineProperty(promise, "_error", {\n get: () => error\n });\n return Promise.reject(error);\n }\n\n this.pendingKeysSet.delete(key);\n\n if (this.done) {\n // Nothing left to abort!\n this.unlistenAbortSignal();\n }\n\n if (error) {\n Object.defineProperty(promise, "_error", {\n get: () => error\n });\n this.emit(false, key);\n return Promise.reject(error);\n }\n\n Object.defineProperty(promise, "_data", {\n get: () => data\n });\n this.emit(false, key);\n return data;\n }\n\n emit(aborted, settledKey) {\n this.subscribers.forEach(subscriber => subscriber(aborted, settledKey));\n }\n\n subscribe(fn) {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n\n cancel() {\n this.controller.abort();\n this.pendingKeysSet.forEach((v, k) => this.pendingKeysSet.delete(k));\n this.emit(true);\n }\n\n async resolveData(signal) {\n let aborted = false;\n\n if (!this.done) {\n let onAbort = () => this.cancel();\n\n signal.addEventListener("abort", onAbort);\n aborted = await new Promise(resolve => {\n this.subscribe(aborted => {\n signal.removeEventListener("abort", onAbort);\n\n if (aborted || this.done) {\n resolve(aborted);\n }\n });\n });\n }\n\n return aborted;\n }\n\n get done() {\n return this.pendingKeysSet.size === 0;\n }\n\n get unwrappedData() {\n invariant(this.data !== null && this.done, "Can only unwrap data on initialized and settled deferreds");\n return Object.entries(this.data).reduce((acc, _ref2) => {\n let [key, value] = _ref2;\n return Object.assign(acc, {\n [key]: unwrapTrackedPromise(value)\n });\n }, {});\n }\n\n get pendingKeys() {\n return Array.from(this.pendingKeysSet);\n }\n\n}\n\nfunction isTrackedPromise(value) {\n return value instanceof Promise && value._tracked === true;\n}\n\nfunction unwrapTrackedPromise(value) {\n if (!isTrackedPromise(value)) {\n return value;\n }\n\n if (value._error) {\n throw value._error;\n }\n\n return value._data;\n}\n\nconst defer = function defer(data, init) {\n if (init === void 0) {\n init = {};\n }\n\n let responseInit = typeof init === "number" ? {\n status: init\n } : init;\n return new DeferredData(data, responseInit);\n};\n/**\n * A redirect response. Sets the status code and the `Location` header.\n * Defaults to "302 Found".\n */\n\nconst redirect = function redirect(url, init) {\n if (init === void 0) {\n init = 302;\n }\n\n let responseInit = init;\n\n if (typeof responseInit === "number") {\n responseInit = {\n status: responseInit\n };\n } else if (typeof responseInit.status === "undefined") {\n responseInit.status = 302;\n }\n\n let headers = new Headers(responseInit.headers);\n headers.set("Location", url);\n return new Response(null, _extends({}, responseInit, {\n headers\n }));\n};\n/**\n * @private\n * Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies\n */\n\nclass ErrorResponse {\n constructor(status, statusText, data, internal) {\n if (internal === void 0) {\n internal = false;\n }\n\n this.status = status;\n this.statusText = statusText || "";\n this.internal = internal;\n\n if (data instanceof Error) {\n this.data = data.toString();\n this.error = data;\n } else {\n this.data = data;\n }\n }\n\n}\n/**\n * Check if the given error is an ErrorResponse generated from a 4xx/5xx\n * Response thrown from an action/loader\n */\n\nfunction isRouteErrorResponse(error) {\n return error != null && typeof error.status === "number" && typeof error.statusText === "string" && typeof error.internal === "boolean" && "data" in error;\n}\n\nconst validMutationMethodsArr = ["post", "put", "patch", "delete"];\nconst validMutationMethods = new Set(validMutationMethodsArr);\nconst validRequestMethodsArr = ["get", ...validMutationMethodsArr];\nconst validRequestMethods = new Set(validRequestMethodsArr);\nconst redirectStatusCodes = new Set([301, 302, 303, 307, 308]);\nconst redirectPreserveMethodStatusCodes = new Set([307, 308]);\nconst IDLE_NAVIGATION = {\n state: "idle",\n location: undefined,\n formMethod: undefined,\n formAction: undefined,\n formEncType: undefined,\n formData: undefined\n};\nconst IDLE_FETCHER = {\n state: "idle",\n data: undefined,\n formMethod: undefined,\n formAction: undefined,\n formEncType: undefined,\n formData: undefined\n};\nconst IDLE_BLOCKER = {\n state: "unblocked",\n proceed: undefined,\n reset: undefined,\n location: undefined\n};\nconst ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i;\nconst isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";\nconst isServer = !isBrowser; //#endregion\n////////////////////////////////////////////////////////////////////////////////\n//#region createRouter\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * Create a router and listen to history POP navigations\n */\n\nfunction createRouter(init) {\n invariant(init.routes.length > 0, "You must provide a non-empty routes array to createRouter");\n let dataRoutes = convertRoutesToDataRoutes(init.routes);\n let inFlightDataRoutes; // Cleanup function for history\n\n let unlistenHistory = null; // Externally-provided functions to call on all state changes\n\n let subscribers = new Set(); // Externally-provided object to hold scroll restoration locations during routing\n\n let savedScrollPositions = null; // Externally-provided function to get scroll restoration keys\n\n let getScrollRestorationKey = null; // Externally-provided function to get current scroll position\n\n let getScrollPosition = null; // One-time flag to control the initial hydration scroll restoration. Because\n // we don\'t get the saved positions from until _after_\n // the initial render, we need to manually trigger a separate updateState to\n // send along the restoreScrollPosition\n // Set to true if we have `hydrationData` since we assume we were SSR\'d and that\n // SSR did the initial scroll restoration.\n\n let initialScrollRestored = init.hydrationData != null;\n let initialMatches = matchRoutes(dataRoutes, init.history.location, init.basename);\n let initialErrors = null;\n\n if (initialMatches == null) {\n // If we do not match a user-provided-route, fall back to the root\n // to allow the error boundary to take over\n let error = getInternalRouterError(404, {\n pathname: init.history.location.pathname\n });\n let {\n matches,\n route\n } = getShortCircuitMatches(dataRoutes);\n initialMatches = matches;\n initialErrors = {\n [route.id]: error\n };\n }\n\n let initialized = !initialMatches.some(m => m.route.loader) || init.hydrationData != null;\n let router;\n let state = {\n historyAction: init.history.action,\n location: init.history.location,\n matches: initialMatches,\n initialized,\n navigation: IDLE_NAVIGATION,\n // Don\'t restore on initial updateState() if we were SSR\'d\n restoreScrollPosition: init.hydrationData != null ? false : null,\n preventScrollReset: false,\n revalidation: "idle",\n loaderData: init.hydrationData && init.hydrationData.loaderData || {},\n actionData: init.hydrationData && init.hydrationData.actionData || null,\n errors: init.hydrationData && init.hydrationData.errors || initialErrors,\n fetchers: new Map(),\n blockers: new Map()\n }; // -- Stateful internal variables to manage navigations --\n // Current navigation in progress (to be committed in completeNavigation)\n\n let pendingAction = Action.Pop; // Should the current navigation prevent the scroll reset if scroll cannot\n // be restored?\n\n let pendingPreventScrollReset = false; // AbortController for the active navigation\n\n let pendingNavigationController; // We use this to avoid touching history in completeNavigation if a\n // revalidation is entirely uninterrupted\n\n let isUninterruptedRevalidation = false; // Use this internal flag to force revalidation of all loaders:\n // - submissions (completed or interrupted)\n // - useRevalidate()\n // - X-Remix-Revalidate (from redirect)\n\n let isRevalidationRequired = false; // Use this internal array to capture routes that require revalidation due\n // to a cancelled deferred on action submission\n\n let cancelledDeferredRoutes = []; // Use this internal array to capture fetcher loads that were cancelled by an\n // action navigation and require revalidation\n\n let cancelledFetcherLoads = []; // AbortControllers for any in-flight fetchers\n\n let fetchControllers = new Map(); // Track loads based on the order in which they started\n\n let incrementingLoadId = 0; // Track the outstanding pending navigation data load to be compared against\n // the globally incrementing load when a fetcher load lands after a completed\n // navigation\n\n let pendingNavigationLoadId = -1; // Fetchers that triggered data reloads as a result of their actions\n\n let fetchReloadIds = new Map(); // Fetchers that triggered redirect navigations from their actions\n\n let fetchRedirectIds = new Set(); // Most recent href/match for fetcher.load calls for fetchers\n\n let fetchLoadMatches = new Map(); // Store DeferredData instances for active route matches. When a\n // route loader returns defer() we stick one in here. Then, when a nested\n // promise resolves we update loaderData. If a new navigation starts we\n // cancel active deferreds for eliminated routes.\n\n let activeDeferreds = new Map(); // Store blocker functions in a separate Map outside of router state since\n // we don\'t need to update UI state if they change\n\n let blockerFunctions = new Map(); // Flag to ignore the next history update, so we can revert the URL change on\n // a POP navigation that was blocked by the user without touching router state\n\n let ignoreNextHistoryUpdate = false; // Initialize the router, all side effects should be kicked off from here.\n // Implemented as a Fluent API for ease of:\n // let router = createRouter(init).initialize();\n\n function initialize() {\n // If history informs us of a POP navigation, start the navigation but do not update\n // state. We\'ll update our own state once the navigation completes\n unlistenHistory = init.history.listen(_ref => {\n let {\n action: historyAction,\n location,\n delta\n } = _ref;\n\n // Ignore this event if it was just us resetting the URL from a\n // blocked POP navigation\n if (ignoreNextHistoryUpdate) {\n ignoreNextHistoryUpdate = false;\n return;\n }\n\n warning(blockerFunctions.size === 0 || delta != null, "You are trying to use a blocker on a POP navigation to a location " + "that was not created by @remix-run/router. This will fail silently in " + "production. This can happen if you are navigating outside the router " + "via `window.history.pushState`/`window.location.hash` instead of using " + "router navigation APIs. This can also happen if you are using " + "createHashRouter and the user manually changes the URL.");\n let blockerKey = shouldBlockNavigation({\n currentLocation: state.location,\n nextLocation: location,\n historyAction\n });\n\n if (blockerKey && delta != null) {\n // Restore the URL to match the current UI, but don\'t update router state\n ignoreNextHistoryUpdate = true;\n init.history.go(delta * -1); // Put the blocker into a blocked state\n\n updateBlocker(blockerKey, {\n state: "blocked",\n location,\n\n proceed() {\n updateBlocker(blockerKey, {\n state: "proceeding",\n proceed: undefined,\n reset: undefined,\n location\n }); // Re-do the same POP navigation we just blocked\n\n init.history.go(delta);\n },\n\n reset() {\n deleteBlocker(blockerKey);\n updateState({\n blockers: new Map(router.state.blockers)\n });\n }\n\n });\n return;\n }\n\n return startNavigation(historyAction, location);\n }); // Kick off initial data load if needed. Use Pop to avoid modifying history\n\n if (!state.initialized) {\n startNavigation(Action.Pop, state.location);\n }\n\n return router;\n } // Clean up a router and it\'s side effects\n\n\n function dispose() {\n if (unlistenHistory) {\n unlistenHistory();\n }\n\n subscribers.clear();\n pendingNavigationController && pendingNavigationController.abort();\n state.fetchers.forEach((_, key) => deleteFetcher(key));\n state.blockers.forEach((_, key) => deleteBlocker(key));\n } // Subscribe to state updates for the router\n\n\n function subscribe(fn) {\n subscribers.add(fn);\n return () => subscribers.delete(fn);\n } // Update our state and notify the calling context of the change\n\n\n function updateState(newState) {\n state = _extends({}, state, newState);\n subscribers.forEach(subscriber => subscriber(state));\n } // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION\n // and setting state.[historyAction/location/matches] to the new route.\n // - Location is a required param\n // - Navigation will always be set to IDLE_NAVIGATION\n // - Can pass any other state in newState\n\n\n function completeNavigation(location, newState) {\n var _location$state, _location$state2;\n\n // Deduce if we\'re in a loading/actionReload state:\n // - We have committed actionData in the store\n // - The current navigation was a mutation submission\n // - We\'re past the submitting state and into the loading state\n // - The location being loaded is not the result of a redirect\n let isActionReload = state.actionData != null && state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && state.navigation.state === "loading" && ((_location$state = location.state) == null ? void 0 : _location$state._isRedirect) !== true;\n let actionData;\n\n if (newState.actionData) {\n if (Object.keys(newState.actionData).length > 0) {\n actionData = newState.actionData;\n } else {\n // Empty actionData -> clear prior actionData due to an action error\n actionData = null;\n }\n } else if (isActionReload) {\n // Keep the current data if we\'re wrapping up the action reload\n actionData = state.actionData;\n } else {\n // Clear actionData on any other completed navigations\n actionData = null;\n } // Always preserve any existing loaderData from re-used routes\n\n\n let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData; // On a successful navigation we can assume we got through all blockers\n // so we can start fresh\n\n for (let [key] of blockerFunctions) {\n deleteBlocker(key);\n } // Always respect the user flag. Otherwise don\'t reset on mutation\n // submission navigations unless they redirect\n\n\n let preventScrollReset = pendingPreventScrollReset === true || state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && ((_location$state2 = location.state) == null ? void 0 : _location$state2._isRedirect) !== true;\n\n if (inFlightDataRoutes) {\n dataRoutes = inFlightDataRoutes;\n inFlightDataRoutes = undefined;\n }\n\n updateState(_extends({}, newState, {\n actionData,\n loaderData,\n historyAction: pendingAction,\n location,\n initialized: true,\n navigation: IDLE_NAVIGATION,\n revalidation: "idle",\n restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),\n preventScrollReset,\n blockers: new Map(state.blockers)\n }));\n\n if (isUninterruptedRevalidation) ; else if (pendingAction === Action.Pop) ; else if (pendingAction === Action.Push) {\n init.history.push(location, location.state);\n } else if (pendingAction === Action.Replace) {\n init.history.replace(location, location.state);\n } // Reset stateful navigation vars\n\n\n pendingAction = Action.Pop;\n pendingPreventScrollReset = false;\n isUninterruptedRevalidation = false;\n isRevalidationRequired = false;\n cancelledDeferredRoutes = [];\n cancelledFetcherLoads = [];\n } // Trigger a navigation event, which can either be a numerical POP or a PUSH\n // replace with an optional submission\n\n\n async function navigate(to, opts) {\n if (typeof to === "number") {\n init.history.go(to);\n return;\n }\n\n let {\n path,\n submission,\n error\n } = normalizeNavigateOptions(to, opts);\n let currentLocation = state.location;\n let nextLocation = createLocation(state.location, path, opts && opts.state); // When using navigate as a PUSH/REPLACE we aren\'t reading an already-encoded\n // URL from window.location, so we need to encode it here so the behavior\n // remains the same as POP and non-data-router usages. new URL() does all\n // the same encoding we\'d get from a history.pushState/window.location read\n // without having to touch history\n\n nextLocation = _extends({}, nextLocation, init.history.encodeLocation(nextLocation));\n let userReplace = opts && opts.replace != null ? opts.replace : undefined;\n let historyAction = Action.Push;\n\n if (userReplace === true) {\n historyAction = Action.Replace;\n } else if (userReplace === false) ; else if (submission != null && isMutationMethod(submission.formMethod) && submission.formAction === state.location.pathname + state.location.search) {\n // By default on submissions to the current location we REPLACE so that\n // users don\'t have to double-click the back button to get to the prior\n // location. If the user redirects to a different location from the\n // action/loader this will be ignored and the redirect will be a PUSH\n historyAction = Action.Replace;\n }\n\n let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;\n let blockerKey = shouldBlockNavigation({\n currentLocation,\n nextLocation,\n historyAction\n });\n\n if (blockerKey) {\n // Put the blocker into a blocked state\n updateBlocker(blockerKey, {\n state: "blocked",\n location: nextLocation,\n\n proceed() {\n updateBlocker(blockerKey, {\n state: "proceeding",\n proceed: undefined,\n reset: undefined,\n location: nextLocation\n }); // Send the same navigation through\n\n navigate(to, opts);\n },\n\n reset() {\n deleteBlocker(blockerKey);\n updateState({\n blockers: new Map(state.blockers)\n });\n }\n\n });\n return;\n }\n\n return await startNavigation(historyAction, nextLocation, {\n submission,\n // Send through the formData serialization error if we have one so we can\n // render at the right error boundary after we match routes\n pendingError: error,\n preventScrollReset,\n replace: opts && opts.replace\n });\n } // Revalidate all current loaders. If a navigation is in progress or if this\n // is interrupted by a navigation, allow this to "succeed" by calling all\n // loaders during the next loader round\n\n\n function revalidate() {\n interruptActiveLoads();\n updateState({\n revalidation: "loading"\n }); // If we\'re currently submitting an action, we don\'t need to start a new\n // navigation, we\'ll just let the follow up loader execution call all loaders\n\n if (state.navigation.state === "submitting") {\n return;\n } // If we\'re currently in an idle state, start a new navigation for the current\n // action/location and mark it as uninterrupted, which will skip the history\n // update in completeNavigation\n\n\n if (state.navigation.state === "idle") {\n startNavigation(state.historyAction, state.location, {\n startUninterruptedRevalidation: true\n });\n return;\n } // Otherwise, if we\'re currently in a loading state, just start a new\n // navigation to the navigation.location but do not trigger an uninterrupted\n // revalidation so that history correctly updates once the navigation completes\n\n\n startNavigation(pendingAction || state.historyAction, state.navigation.location, {\n overrideNavigation: state.navigation\n });\n } // Start a navigation to the given action/location. Can optionally provide a\n // overrideNavigation which will override the normalLoad in the case of a redirect\n // navigation\n\n\n async function startNavigation(historyAction, location, opts) {\n // Abort any in-progress navigations and start a new one. Unset any ongoing\n // uninterrupted revalidations unless told otherwise, since we want this\n // new navigation to update history normally\n pendingNavigationController && pendingNavigationController.abort();\n pendingNavigationController = null;\n pendingAction = historyAction;\n isUninterruptedRevalidation = (opts && opts.startUninterruptedRevalidation) === true; // Save the current scroll position every time we start a new navigation,\n // and track whether we should reset scroll on completion\n\n saveScrollPosition(state.location, state.matches);\n pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;\n let routesToUse = inFlightDataRoutes || dataRoutes;\n let loadingNavigation = opts && opts.overrideNavigation;\n let matches = matchRoutes(routesToUse, location, init.basename); // Short circuit with a 404 on the root error boundary if we match nothing\n\n if (!matches) {\n let error = getInternalRouterError(404, {\n pathname: location.pathname\n });\n let {\n matches: notFoundMatches,\n route\n } = getShortCircuitMatches(routesToUse); // Cancel all pending deferred on 404s since we don\'t keep any routes\n\n cancelActiveDeferreds();\n completeNavigation(location, {\n matches: notFoundMatches,\n loaderData: {},\n errors: {\n [route.id]: error\n }\n });\n return;\n } // Short circuit if it\'s only a hash change and not a mutation submission\n // For example, on /page#hash and submit a
which will\n // default to a navigation to /page\n\n\n if (isHashChangeOnly(state.location, location) && !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))) {\n completeNavigation(location, {\n matches\n });\n return;\n } // Create a controller/Request for this navigation\n\n\n pendingNavigationController = new AbortController();\n let request = createClientSideRequest(init.history, location, pendingNavigationController.signal, opts && opts.submission);\n let pendingActionData;\n let pendingError;\n\n if (opts && opts.pendingError) {\n // If we have a pendingError, it means the user attempted a GET submission\n // with binary FormData so assign here and skip to handleLoaders. That\n // way we handle calling loaders above the boundary etc. It\'s not really\n // different from an actionError in that sense.\n pendingError = {\n [findNearestBoundary(matches).route.id]: opts.pendingError\n };\n } else if (opts && opts.submission && isMutationMethod(opts.submission.formMethod)) {\n // Call action if we received an action submission\n let actionOutput = await handleAction(request, location, opts.submission, matches, {\n replace: opts.replace\n });\n\n if (actionOutput.shortCircuited) {\n return;\n }\n\n pendingActionData = actionOutput.pendingActionData;\n pendingError = actionOutput.pendingActionError;\n\n let navigation = _extends({\n state: "loading",\n location\n }, opts.submission);\n\n loadingNavigation = navigation; // Create a GET request for the loaders\n\n request = new Request(request.url, {\n signal: request.signal\n });\n } // Call loaders\n\n\n let {\n shortCircuited,\n loaderData,\n errors\n } = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.replace, pendingActionData, pendingError);\n\n if (shortCircuited) {\n return;\n } // Clean up now that the action/loaders have completed. Don\'t clean up if\n // we short circuited because pendingNavigationController will have already\n // been assigned to a new controller for the next navigation\n\n\n pendingNavigationController = null;\n completeNavigation(location, _extends({\n matches\n }, pendingActionData ? {\n actionData: pendingActionData\n } : {}, {\n loaderData,\n errors\n }));\n } // Call the action matched by the leaf route for this navigation and handle\n // redirects/errors\n\n\n async function handleAction(request, location, submission, matches, opts) {\n interruptActiveLoads(); // Put us in a submitting state\n\n let navigation = _extends({\n state: "submitting",\n location\n }, submission);\n\n updateState({\n navigation\n }); // Call our action and get the result\n\n let result;\n let actionMatch = getTargetMatch(matches, location);\n\n if (!actionMatch.route.action) {\n result = {\n type: ResultType.error,\n error: getInternalRouterError(405, {\n method: request.method,\n pathname: location.pathname,\n routeId: actionMatch.route.id\n })\n };\n } else {\n result = await callLoaderOrAction("action", request, actionMatch, matches, router.basename);\n\n if (request.signal.aborted) {\n return {\n shortCircuited: true\n };\n }\n }\n\n if (isRedirectResult(result)) {\n let replace;\n\n if (opts && opts.replace != null) {\n replace = opts.replace;\n } else {\n // If the user didn\'t explicity indicate replace behavior, replace if\n // we redirected to the exact same location we\'re currently at to avoid\n // double back-buttons\n replace = result.location === state.location.pathname + state.location.search;\n }\n\n await startRedirectNavigation(state, result, {\n submission,\n replace\n });\n return {\n shortCircuited: true\n };\n }\n\n if (isErrorResult(result)) {\n // Store off the pending error - we use it to determine which loaders\n // to call and will commit it when we complete the navigation\n let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id); // By default, all submissions are REPLACE navigations, but if the\n // action threw an error that\'ll be rendered in an errorElement, we fall\n // back to PUSH so that the user can use the back button to get back to\n // the pre-submission form location to try again\n\n if ((opts && opts.replace) !== true) {\n pendingAction = Action.Push;\n }\n\n return {\n // Send back an empty object we can use to clear out any prior actionData\n pendingActionData: {},\n pendingActionError: {\n [boundaryMatch.route.id]: result.error\n }\n };\n }\n\n if (isDeferredResult(result)) {\n throw getInternalRouterError(400, {\n type: "defer-action"\n });\n }\n\n return {\n pendingActionData: {\n [actionMatch.route.id]: result.data\n }\n };\n } // Call all applicable loaders for the given matches, handling redirects,\n // errors, etc.\n\n\n async function handleLoaders(request, location, matches, overrideNavigation, submission, replace, pendingActionData, pendingError) {\n // Figure out the right navigation we want to use for data loading\n let loadingNavigation = overrideNavigation;\n\n if (!loadingNavigation) {\n let navigation = _extends({\n state: "loading",\n location,\n formMethod: undefined,\n formAction: undefined,\n formEncType: undefined,\n formData: undefined\n }, submission);\n\n loadingNavigation = navigation;\n } // If this was a redirect from an action we don\'t have a "submission" but\n // we have it on the loading navigation so use that if available\n\n\n let activeSubmission = submission ? submission : loadingNavigation.formMethod && loadingNavigation.formAction && loadingNavigation.formData && loadingNavigation.formEncType ? {\n formMethod: loadingNavigation.formMethod,\n formAction: loadingNavigation.formAction,\n formData: loadingNavigation.formData,\n formEncType: loadingNavigation.formEncType\n } : undefined;\n let routesToUse = inFlightDataRoutes || dataRoutes;\n let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, init.basename, pendingActionData, pendingError); // Cancel pending deferreds for no-longer-matched routes or routes we\'re\n // about to reload. Note that if this is an action reload we would have\n // already cancelled all pending deferreds so this would be a no-op\n\n cancelActiveDeferreds(routeId => !(matches && matches.some(m => m.route.id === routeId)) || matchesToLoad && matchesToLoad.some(m => m.route.id === routeId)); // Short circuit if we have no loaders to run\n\n if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {\n completeNavigation(location, _extends({\n matches,\n loaderData: {},\n // Commit pending error if we\'re short circuiting\n errors: pendingError || null\n }, pendingActionData ? {\n actionData: pendingActionData\n } : {}));\n return {\n shortCircuited: true\n };\n } // If this is an uninterrupted revalidation, we remain in our current idle\n // state. If not, we need to switch to our loading state and load data,\n // preserving any new action data or existing action data (in the case of\n // a revalidation interrupting an actionReload)\n\n\n if (!isUninterruptedRevalidation) {\n revalidatingFetchers.forEach(rf => {\n let fetcher = state.fetchers.get(rf.key);\n let revalidatingFetcher = {\n state: "loading",\n data: fetcher && fetcher.data,\n formMethod: undefined,\n formAction: undefined,\n formEncType: undefined,\n formData: undefined,\n " _hasFetcherDoneAnything ": true\n };\n state.fetchers.set(rf.key, revalidatingFetcher);\n });\n let actionData = pendingActionData || state.actionData;\n updateState(_extends({\n navigation: loadingNavigation\n }, actionData ? Object.keys(actionData).length === 0 ? {\n actionData: null\n } : {\n actionData\n } : {}, revalidatingFetchers.length > 0 ? {\n fetchers: new Map(state.fetchers)\n } : {}));\n }\n\n pendingNavigationLoadId = ++incrementingLoadId;\n revalidatingFetchers.forEach(rf => fetchControllers.set(rf.key, pendingNavigationController));\n let {\n results,\n loaderResults,\n fetcherResults\n } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, request);\n\n if (request.signal.aborted) {\n return {\n shortCircuited: true\n };\n } // Clean up _after_ loaders have completed. Don\'t clean up if we short\n // circuited because fetchControllers would have been aborted and\n // reassigned to new controllers for the next navigation\n\n\n revalidatingFetchers.forEach(rf => fetchControllers.delete(rf.key)); // If any loaders returned a redirect Response, start a new REPLACE navigation\n\n let redirect = findRedirect(results);\n\n if (redirect) {\n await startRedirectNavigation(state, redirect, {\n replace\n });\n return {\n shortCircuited: true\n };\n } // Process and commit output from loaders\n\n\n let {\n loaderData,\n errors\n } = processLoaderData(state, matches, matchesToLoad, loaderResults, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds); // Wire up subscribers to update loaderData as promises settle\n\n activeDeferreds.forEach((deferredData, routeId) => {\n deferredData.subscribe(aborted => {\n // Note: No need to updateState here since the TrackedPromise on\n // loaderData is stable across resolve/reject\n // Remove this instance if we were aborted or if promises have settled\n if (aborted || deferredData.done) {\n activeDeferreds.delete(routeId);\n }\n });\n });\n markFetchRedirectsDone();\n let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);\n return _extends({\n loaderData,\n errors\n }, didAbortFetchLoads || revalidatingFetchers.length > 0 ? {\n fetchers: new Map(state.fetchers)\n } : {});\n }\n\n function getFetcher(key) {\n return state.fetchers.get(key) || IDLE_FETCHER;\n } // Trigger a fetcher load/submit for the given fetcher key\n\n\n function fetch(key, routeId, href, opts) {\n if (isServer) {\n throw new Error("router.fetch() was called during the server render, but it shouldn\'t be. " + "You are likely calling a useFetcher() method in the body of your component. " + "Try moving it to a useEffect or a callback.");\n }\n\n if (fetchControllers.has(key)) abortFetcher(key);\n let routesToUse = inFlightDataRoutes || dataRoutes;\n let matches = matchRoutes(routesToUse, href, init.basename);\n\n if (!matches) {\n setFetcherError(key, routeId, getInternalRouterError(404, {\n pathname: href\n }));\n return;\n }\n\n let {\n path,\n submission\n } = normalizeNavigateOptions(href, opts, true);\n let match = getTargetMatch(matches, path);\n pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;\n\n if (submission && isMutationMethod(submission.formMethod)) {\n handleFetcherAction(key, routeId, path, match, matches, submission);\n return;\n } // Store off the match so we can call it\'s shouldRevalidate on subsequent\n // revalidations\n\n\n fetchLoadMatches.set(key, {\n routeId,\n path\n });\n handleFetcherLoader(key, routeId, path, match, matches, submission);\n } // Call the action for the matched fetcher.submit(), and then handle redirects,\n // errors, and revalidation\n\n\n async function handleFetcherAction(key, routeId, path, match, requestMatches, submission) {\n interruptActiveLoads();\n fetchLoadMatches.delete(key);\n\n if (!match.route.action) {\n let error = getInternalRouterError(405, {\n method: submission.formMethod,\n pathname: path,\n routeId: routeId\n });\n setFetcherError(key, routeId, error);\n return;\n } // Put this fetcher into it\'s submitting state\n\n\n let existingFetcher = state.fetchers.get(key);\n\n let fetcher = _extends({\n state: "submitting"\n }, submission, {\n data: existingFetcher && existingFetcher.data,\n " _hasFetcherDoneAnything ": true\n });\n\n state.fetchers.set(key, fetcher);\n updateState({\n fetchers: new Map(state.fetchers)\n }); // Call the action for the fetcher\n\n let abortController = new AbortController();\n let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);\n fetchControllers.set(key, abortController);\n let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, router.basename);\n\n if (fetchRequest.signal.aborted) {\n // We can delete this so long as we weren\'t aborted by ou our own fetcher\n // re-submit which would have put _new_ controller is in fetchControllers\n if (fetchControllers.get(key) === abortController) {\n fetchControllers.delete(key);\n }\n\n return;\n }\n\n if (isRedirectResult(actionResult)) {\n fetchControllers.delete(key);\n fetchRedirectIds.add(key);\n\n let loadingFetcher = _extends({\n state: "loading"\n }, submission, {\n data: undefined,\n " _hasFetcherDoneAnything ": true\n });\n\n state.fetchers.set(key, loadingFetcher);\n updateState({\n fetchers: new Map(state.fetchers)\n });\n return startRedirectNavigation(state, actionResult, {\n isFetchActionRedirect: true\n });\n } // Process any non-redirect errors thrown\n\n\n if (isErrorResult(actionResult)) {\n setFetcherError(key, routeId, actionResult.error);\n return;\n }\n\n if (isDeferredResult(actionResult)) {\n throw getInternalRouterError(400, {\n type: "defer-action"\n });\n } // Start the data load for current matches, or the next location if we\'re\n // in the middle of a navigation\n\n\n let nextLocation = state.navigation.location || state.location;\n let revalidationRequest = createClientSideRequest(init.history, nextLocation, abortController.signal);\n let routesToUse = inFlightDataRoutes || dataRoutes;\n let matches = state.navigation.state !== "idle" ? matchRoutes(routesToUse, state.navigation.location, init.basename) : state.matches;\n invariant(matches, "Didn\'t find any matches after fetcher action");\n let loadId = ++incrementingLoadId;\n fetchReloadIds.set(key, loadId);\n\n let loadFetcher = _extends({\n state: "loading",\n data: actionResult.data\n }, submission, {\n " _hasFetcherDoneAnything ": true\n });\n\n state.fetchers.set(key, loadFetcher);\n let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, init.basename, {\n [match.route.id]: actionResult.data\n }, undefined // No need to send through errors since we short circuit above\n ); // Put all revalidating fetchers into the loading state, except for the\n // current fetcher which we want to keep in it\'s current loading state which\n // contains it\'s action submission info + action data\n\n revalidatingFetchers.filter(rf => rf.key !== key).forEach(rf => {\n let staleKey = rf.key;\n let existingFetcher = state.fetchers.get(staleKey);\n let revalidatingFetcher = {\n state: "loading",\n data: existingFetcher && existingFetcher.data,\n formMethod: undefined,\n formAction: undefined,\n formEncType: undefined,\n formData: undefined,\n " _hasFetcherDoneAnything ": true\n };\n state.fetchers.set(staleKey, revalidatingFetcher);\n fetchControllers.set(staleKey, abortController);\n });\n updateState({\n fetchers: new Map(state.fetchers)\n });\n let {\n results,\n loaderResults,\n fetcherResults\n } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, revalidationRequest);\n\n if (abortController.signal.aborted) {\n return;\n }\n\n fetchReloadIds.delete(key);\n fetchControllers.delete(key);\n revalidatingFetchers.forEach(r => fetchControllers.delete(r.key));\n let redirect = findRedirect(results);\n\n if (redirect) {\n return startRedirectNavigation(state, redirect);\n } // Process and commit output from loaders\n\n\n let {\n loaderData,\n errors\n } = processLoaderData(state, state.matches, matchesToLoad, loaderResults, undefined, revalidatingFetchers, fetcherResults, activeDeferreds);\n let doneFetcher = {\n state: "idle",\n data: actionResult.data,\n formMethod: undefined,\n formAction: undefined,\n formEncType: undefined,\n formData: undefined,\n " _hasFetcherDoneAnything ": true\n };\n state.fetchers.set(key, doneFetcher);\n let didAbortFetchLoads = abortStaleFetchLoads(loadId); // If we are currently in a navigation loading state and this fetcher is\n // more recent than the navigation, we want the newer data so abort the\n // navigation and complete it with the fetcher data\n\n if (state.navigation.state === "loading" && loadId > pendingNavigationLoadId) {\n invariant(pendingAction, "Expected pending action");\n pendingNavigationController && pendingNavigationController.abort();\n completeNavigation(state.navigation.location, {\n matches,\n loaderData,\n errors,\n fetchers: new Map(state.fetchers)\n });\n } else {\n // otherwise just update with the fetcher data, preserving any existing\n // loaderData for loaders that did not need to reload. We have to\n // manually merge here since we aren\'t going through completeNavigation\n updateState(_extends({\n errors,\n loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors)\n }, didAbortFetchLoads ? {\n fetchers: new Map(state.fetchers)\n } : {}));\n isRevalidationRequired = false;\n }\n } // Call the matched loader for fetcher.load(), handling redirects, errors, etc.\n\n\n async function handleFetcherLoader(key, routeId, path, match, matches, submission) {\n let existingFetcher = state.fetchers.get(key); // Put this fetcher into it\'s loading state\n\n let loadingFetcher = _extends({\n state: "loading",\n formMethod: undefined,\n formAction: undefined,\n formEncType: undefined,\n formData: undefined\n }, submission, {\n data: existingFetcher && existingFetcher.data,\n " _hasFetcherDoneAnything ": true\n });\n\n state.fetchers.set(key, loadingFetcher);\n updateState({\n fetchers: new Map(state.fetchers)\n }); // Call the loader for this fetcher route match\n\n let abortController = new AbortController();\n let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);\n fetchControllers.set(key, abortController);\n let result = await callLoaderOrAction("loader", fetchRequest, match, matches, router.basename); // Deferred isn\'t supported for fetcher loads, await everything and treat it\n // as a normal load. resolveDeferredData will return undefined if this\n // fetcher gets aborted, so we just leave result untouched and short circuit\n // below if that happens\n\n if (isDeferredResult(result)) {\n result = (await resolveDeferredData(result, fetchRequest.signal, true)) || result;\n } // We can delete this so long as we weren\'t aborted by ou our own fetcher\n // re-load which would have put _new_ controller is in fetchControllers\n\n\n if (fetchControllers.get(key) === abortController) {\n fetchControllers.delete(key);\n }\n\n if (fetchRequest.signal.aborted) {\n return;\n } // If the loader threw a redirect Response, start a new REPLACE navigation\n\n\n if (isRedirectResult(result)) {\n await startRedirectNavigation(state, result);\n return;\n } // Process any non-redirect errors thrown\n\n\n if (isErrorResult(result)) {\n let boundaryMatch = findNearestBoundary(state.matches, routeId);\n state.fetchers.delete(key); // TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch -\n // do we need to behave any differently with our non-redirect errors?\n // What if it was a non-redirect Response?\n\n updateState({\n fetchers: new Map(state.fetchers),\n errors: {\n [boundaryMatch.route.id]: result.error\n }\n });\n return;\n }\n\n invariant(!isDeferredResult(result), "Unhandled fetcher deferred data"); // Put the fetcher back into an idle state\n\n let doneFetcher = {\n state: "idle",\n data: result.data,\n formMethod: undefined,\n formAction: undefined,\n formEncType: undefined,\n formData: undefined,\n " _hasFetcherDoneAnything ": true\n };\n state.fetchers.set(key, doneFetcher);\n updateState({\n fetchers: new Map(state.fetchers)\n });\n }\n /**\n * Utility function to handle redirects returned from an action or loader.\n * Normally, a redirect "replaces" the navigation that triggered it. So, for\n * example:\n *\n * - user is on /a\n * - user clicks a link to /b\n * - loader for /b redirects to /c\n *\n * In a non-JS app the browser would track the in-flight navigation to /b and\n * then replace it with /c when it encountered the redirect response. In\n * the end it would only ever update the URL bar with /c.\n *\n * In client-side routing using pushState/replaceState, we aim to emulate\n * this behavior and we also do not update history until the end of the\n * navigation (including processed redirects). This means that we never\n * actually touch history until we\'ve processed redirects, so we just use\n * the history action from the original navigation (PUSH or REPLACE).\n */\n\n\n async function startRedirectNavigation(state, redirect, _temp) {\n var _window;\n\n let {\n submission,\n replace,\n isFetchActionRedirect\n } = _temp === void 0 ? {} : _temp;\n\n if (redirect.revalidate) {\n isRevalidationRequired = true;\n }\n\n let redirectLocation = createLocation(state.location, redirect.location, // TODO: This can be removed once we get rid of useTransition in Remix v2\n _extends({\n _isRedirect: true\n }, isFetchActionRedirect ? {\n _isFetchActionRedirect: true\n } : {}));\n invariant(redirectLocation, "Expected a location on the redirect navigation"); // Check if this an absolute external redirect that goes to a new origin\n\n if (ABSOLUTE_URL_REGEX.test(redirect.location) && isBrowser && typeof ((_window = window) == null ? void 0 : _window.location) !== "undefined") {\n let url = init.history.createURL(redirect.location);\n let isDifferentBasename = stripBasename(url.pathname, init.basename || "/") == null;\n\n if (window.location.origin !== url.origin || isDifferentBasename) {\n if (replace) {\n window.location.replace(redirect.location);\n } else {\n window.location.assign(redirect.location);\n }\n\n return;\n }\n } // There\'s no need to abort on redirects, since we don\'t detect the\n // redirect until the action/loaders have settled\n\n\n pendingNavigationController = null;\n let redirectHistoryAction = replace === true ? Action.Replace : Action.Push; // Use the incoming submission if provided, fallback on the active one in\n // state.navigation\n\n let {\n formMethod,\n formAction,\n formEncType,\n formData\n } = state.navigation;\n\n if (!submission && formMethod && formAction && formData && formEncType) {\n submission = {\n formMethod,\n formAction,\n formEncType,\n formData\n };\n } // If this was a 307/308 submission we want to preserve the HTTP method and\n // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the\n // redirected location\n\n\n if (redirectPreserveMethodStatusCodes.has(redirect.status) && submission && isMutationMethod(submission.formMethod)) {\n await startNavigation(redirectHistoryAction, redirectLocation, {\n submission: _extends({}, submission, {\n formAction: redirect.location\n }),\n // Preserve this flag across redirects\n preventScrollReset: pendingPreventScrollReset\n });\n } else {\n // Otherwise, we kick off a new loading navigation, preserving the\n // submission info for the duration of this navigation\n await startNavigation(redirectHistoryAction, redirectLocation, {\n overrideNavigation: {\n state: "loading",\n location: redirectLocation,\n formMethod: submission ? submission.formMethod : undefined,\n formAction: submission ? submission.formAction : undefined,\n formEncType: submission ? submission.formEncType : undefined,\n formData: submission ? submission.formData : undefined\n },\n // Preserve this flag across redirects\n preventScrollReset: pendingPreventScrollReset\n });\n }\n }\n\n async function callLoadersAndMaybeResolveData(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {\n // Call all navigation loaders and revalidating fetcher loaders in parallel,\n // then slice off the results into separate arrays so we can handle them\n // accordingly\n let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, router.basename)), ...fetchersToLoad.map(f => {\n if (f.matches && f.match) {\n return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, request.signal), f.match, f.matches, router.basename);\n } else {\n let error = {\n type: ResultType.error,\n error: getInternalRouterError(404, {\n pathname: f.path\n })\n };\n return error;\n }\n })]);\n let loaderResults = results.slice(0, matchesToLoad.length);\n let fetcherResults = results.slice(matchesToLoad.length);\n await Promise.all([resolveDeferredResults(currentMatches, matchesToLoad, loaderResults, request.signal, false, state.loaderData), resolveDeferredResults(currentMatches, fetchersToLoad.map(f => f.match), fetcherResults, request.signal, true)]);\n return {\n results,\n loaderResults,\n fetcherResults\n };\n }\n\n function interruptActiveLoads() {\n // Every interruption triggers a revalidation\n isRevalidationRequired = true; // Cancel pending route-level deferreds and mark cancelled routes for\n // revalidation\n\n cancelledDeferredRoutes.push(...cancelActiveDeferreds()); // Abort in-flight fetcher loads\n\n fetchLoadMatches.forEach((_, key) => {\n if (fetchControllers.has(key)) {\n cancelledFetcherLoads.push(key);\n abortFetcher(key);\n }\n });\n }\n\n function setFetcherError(key, routeId, error) {\n let boundaryMatch = findNearestBoundary(state.matches, routeId);\n deleteFetcher(key);\n updateState({\n errors: {\n [boundaryMatch.route.id]: error\n },\n fetchers: new Map(state.fetchers)\n });\n }\n\n function deleteFetcher(key) {\n if (fetchControllers.has(key)) abortFetcher(key);\n fetchLoadMatches.delete(key);\n fetchReloadIds.delete(key);\n fetchRedirectIds.delete(key);\n state.fetchers.delete(key);\n }\n\n function abortFetcher(key) {\n let controller = fetchControllers.get(key);\n invariant(controller, "Expected fetch controller: " + key);\n controller.abort();\n fetchControllers.delete(key);\n }\n\n function markFetchersDone(keys) {\n for (let key of keys) {\n let fetcher = getFetcher(key);\n let doneFetcher = {\n state: "idle",\n data: fetcher.data,\n formMethod: undefined,\n formAction: undefined,\n formEncType: undefined,\n formData: undefined,\n " _hasFetcherDoneAnything ": true\n };\n state.fetchers.set(key, doneFetcher);\n }\n }\n\n function markFetchRedirectsDone() {\n let doneKeys = [];\n\n for (let key of fetchRedirectIds) {\n let fetcher = state.fetchers.get(key);\n invariant(fetcher, "Expected fetcher: " + key);\n\n if (fetcher.state === "loading") {\n fetchRedirectIds.delete(key);\n doneKeys.push(key);\n }\n }\n\n markFetchersDone(doneKeys);\n }\n\n function abortStaleFetchLoads(landedId) {\n let yeetedKeys = [];\n\n for (let [key, id] of fetchReloadIds) {\n if (id < landedId) {\n let fetcher = state.fetchers.get(key);\n invariant(fetcher, "Expected fetcher: " + key);\n\n if (fetcher.state === "loading") {\n abortFetcher(key);\n fetchReloadIds.delete(key);\n yeetedKeys.push(key);\n }\n }\n }\n\n markFetchersDone(yeetedKeys);\n return yeetedKeys.length > 0;\n }\n\n function getBlocker(key, fn) {\n let blocker = state.blockers.get(key) || IDLE_BLOCKER;\n\n if (blockerFunctions.get(key) !== fn) {\n blockerFunctions.set(key, fn);\n }\n\n return blocker;\n }\n\n function deleteBlocker(key) {\n state.blockers.delete(key);\n blockerFunctions.delete(key);\n } // Utility function to update blockers, ensuring valid state transitions\n\n\n function updateBlocker(key, newBlocker) {\n let blocker = state.blockers.get(key) || IDLE_BLOCKER; // Poor mans state machine :)\n // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM\n\n invariant(blocker.state === "unblocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "proceeding" || blocker.state === "blocked" && newBlocker.state === "unblocked" || blocker.state === "proceeding" && newBlocker.state === "unblocked", "Invalid blocker state transition: " + blocker.state + " -> " + newBlocker.state);\n state.blockers.set(key, newBlocker);\n updateState({\n blockers: new Map(state.blockers)\n });\n }\n\n function shouldBlockNavigation(_ref2) {\n let {\n currentLocation,\n nextLocation,\n historyAction\n } = _ref2;\n\n if (blockerFunctions.size === 0) {\n return;\n } // We ony support a single active blocker at the moment since we don\'t have\n // any compelling use cases for multi-blocker yet\n\n\n if (blockerFunctions.size > 1) {\n warning(false, "A router only supports one blocker at a time");\n }\n\n let entries = Array.from(blockerFunctions.entries());\n let [blockerKey, blockerFunction] = entries[entries.length - 1];\n let blocker = state.blockers.get(blockerKey);\n\n if (blocker && blocker.state === "proceeding") {\n // If the blocker is currently proceeding, we don\'t need to re-check\n // it and can let this navigation continue\n return;\n } // At this point, we know we\'re unblocked/blocked so we need to check the\n // user-provided blocker function\n\n\n if (blockerFunction({\n currentLocation,\n nextLocation,\n historyAction\n })) {\n return blockerKey;\n }\n }\n\n function cancelActiveDeferreds(predicate) {\n let cancelledRouteIds = [];\n activeDeferreds.forEach((dfd, routeId) => {\n if (!predicate || predicate(routeId)) {\n // Cancel the deferred - but do not remove from activeDeferreds here -\n // we rely on the subscribers to do that so our tests can assert proper\n // cleanup via _internalActiveDeferreds\n dfd.cancel();\n cancelledRouteIds.push(routeId);\n activeDeferreds.delete(routeId);\n }\n });\n return cancelledRouteIds;\n } // Opt in to capturing and reporting scroll positions during navigations,\n // used by the component\n\n\n function enableScrollRestoration(positions, getPosition, getKey) {\n savedScrollPositions = positions;\n getScrollPosition = getPosition;\n\n getScrollRestorationKey = getKey || (location => location.key); // Perform initial hydration scroll restoration, since we miss the boat on\n // the initial updateState() because we\'ve not yet rendered \n // and therefore have no savedScrollPositions available\n\n\n if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) {\n initialScrollRestored = true;\n let y = getSavedScrollPosition(state.location, state.matches);\n\n if (y != null) {\n updateState({\n restoreScrollPosition: y\n });\n }\n }\n\n return () => {\n savedScrollPositions = null;\n getScrollPosition = null;\n getScrollRestorationKey = null;\n };\n }\n\n function saveScrollPosition(location, matches) {\n if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {\n let userMatches = matches.map(m => createUseMatchesMatch(m, state.loaderData));\n let key = getScrollRestorationKey(location, userMatches) || location.key;\n savedScrollPositions[key] = getScrollPosition();\n }\n }\n\n function getSavedScrollPosition(location, matches) {\n if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {\n let userMatches = matches.map(m => createUseMatchesMatch(m, state.loaderData));\n let key = getScrollRestorationKey(location, userMatches) || location.key;\n let y = savedScrollPositions[key];\n\n if (typeof y === "number") {\n return y;\n }\n }\n\n return null;\n }\n\n function _internalSetRoutes(newRoutes) {\n inFlightDataRoutes = newRoutes;\n }\n\n router = {\n get basename() {\n return init.basename;\n },\n\n get state() {\n return state;\n },\n\n get routes() {\n return dataRoutes;\n },\n\n initialize,\n subscribe,\n enableScrollRestoration,\n navigate,\n fetch,\n revalidate,\n // Passthrough to history-aware createHref used by useHref so we get proper\n // hash-aware URLs in DOM paths\n createHref: to => init.history.createHref(to),\n encodeLocation: to => init.history.encodeLocation(to),\n getFetcher,\n deleteFetcher,\n dispose,\n getBlocker,\n deleteBlocker,\n _internalFetchControllers: fetchControllers,\n _internalActiveDeferreds: activeDeferreds,\n // TODO: Remove setRoutes, it\'s temporary to avoid dealing with\n // updating the tree while validating the update algorithm.\n _internalSetRoutes\n };\n return router;\n} //#endregion\n////////////////////////////////////////////////////////////////////////////////\n//#region createStaticHandler\n////////////////////////////////////////////////////////////////////////////////\n\nconst UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");\nfunction createStaticHandler(routes, opts) {\n invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");\n let dataRoutes = convertRoutesToDataRoutes(routes);\n let basename = (opts ? opts.basename : null) || "/";\n /**\n * The query() method is intended for document requests, in which we want to\n * call an optional action and potentially multiple loaders for all nested\n * routes. It returns a StaticHandlerContext object, which is very similar\n * to the router state (location, loaderData, actionData, errors, etc.) and\n * also adds SSR-specific information such as the statusCode and headers\n * from action/loaders Responses.\n *\n * It _should_ never throw and should report all errors through the\n * returned context.errors object, properly associating errors to their error\n * boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be\n * used to emulate React error boundaries during SSr by performing a second\n * pass only down to the boundaryId.\n *\n * The one exception where we do not return a StaticHandlerContext is when a\n * redirect response is returned or thrown from any action/loader. We\n * propagate that out and return the raw Response so the HTTP server can\n * return it directly.\n */\n\n async function query(request, _temp2) {\n let {\n requestContext\n } = _temp2 === void 0 ? {} : _temp2;\n let url = new URL(request.url);\n let method = request.method.toLowerCase();\n let location = createLocation("", createPath(url), null, "default");\n let matches = matchRoutes(dataRoutes, location, basename); // SSR supports HEAD requests while SPA doesn\'t\n\n if (!isValidMethod(method) && method !== "head") {\n let error = getInternalRouterError(405, {\n method\n });\n let {\n matches: methodNotAllowedMatches,\n route\n } = getShortCircuitMatches(dataRoutes);\n return {\n basename,\n location,\n matches: methodNotAllowedMatches,\n loaderData: {},\n actionData: null,\n errors: {\n [route.id]: error\n },\n statusCode: error.status,\n loaderHeaders: {},\n actionHeaders: {},\n activeDeferreds: null\n };\n } else if (!matches) {\n let error = getInternalRouterError(404, {\n pathname: location.pathname\n });\n let {\n matches: notFoundMatches,\n route\n } = getShortCircuitMatches(dataRoutes);\n return {\n basename,\n location,\n matches: notFoundMatches,\n loaderData: {},\n actionData: null,\n errors: {\n [route.id]: error\n },\n statusCode: error.status,\n loaderHeaders: {},\n actionHeaders: {},\n activeDeferreds: null\n };\n }\n\n let result = await queryImpl(request, location, matches, requestContext);\n\n if (isResponse(result)) {\n return result;\n } // When returning StaticHandlerContext, we patch back in the location here\n // since we need it for React Context. But this helps keep our submit and\n // loadRouteData operating on a Request instead of a Location\n\n\n return _extends({\n location,\n basename\n }, result);\n }\n /**\n * The queryRoute() method is intended for targeted route requests, either\n * for fetch ?_data requests or resource route requests. In this case, we\n * are only ever calling a single action or loader, and we are returning the\n * returned value directly. In most cases, this will be a Response returned\n * from the action/loader, but it may be a primitive or other value as well -\n * and in such cases the calling context should handle that accordingly.\n *\n * We do respect the throw/return differentiation, so if an action/loader\n * throws, then this method will throw the value. This is important so we\n * can do proper boundary identification in Remix where a thrown Response\n * must go to the Catch Boundary but a returned Response is happy-path.\n *\n * One thing to note is that any Router-initiated Errors that make sense\n * to associate with a status code will be thrown as an ErrorResponse\n * instance which include the raw Error, such that the calling context can\n * serialize the error as they see fit while including the proper response\n * code. Examples here are 404 and 405 errors that occur prior to reaching\n * any user-defined loaders.\n */\n\n\n async function queryRoute(request, _temp3) {\n let {\n routeId,\n requestContext\n } = _temp3 === void 0 ? {} : _temp3;\n let url = new URL(request.url);\n let method = request.method.toLowerCase();\n let location = createLocation("", createPath(url), null, "default");\n let matches = matchRoutes(dataRoutes, location, basename); // SSR supports HEAD requests while SPA doesn\'t\n\n if (!isValidMethod(method) && method !== "head" && method !== "options") {\n throw getInternalRouterError(405, {\n method\n });\n } else if (!matches) {\n throw getInternalRouterError(404, {\n pathname: location.pathname\n });\n }\n\n let match = routeId ? matches.find(m => m.route.id === routeId) : getTargetMatch(matches, location);\n\n if (routeId && !match) {\n throw getInternalRouterError(403, {\n pathname: location.pathname,\n routeId\n });\n } else if (!match) {\n // This should never hit I don\'t think?\n throw getInternalRouterError(404, {\n pathname: location.pathname\n });\n }\n\n let result = await queryImpl(request, location, matches, requestContext, match);\n\n if (isResponse(result)) {\n return result;\n }\n\n let error = result.errors ? Object.values(result.errors)[0] : undefined;\n\n if (error !== undefined) {\n // If we got back result.errors, that means the loader/action threw\n // _something_ that wasn\'t a Response, but it\'s not guaranteed/required\n // to be an `instanceof Error` either, so we have to use throw here to\n // preserve the "error" state outside of queryImpl.\n throw error;\n } // Pick off the right state value to return\n\n\n if (result.actionData) {\n return Object.values(result.actionData)[0];\n }\n\n if (result.loaderData) {\n var _result$activeDeferre;\n\n let data = Object.values(result.loaderData)[0];\n\n if ((_result$activeDeferre = result.activeDeferreds) != null && _result$activeDeferre[match.route.id]) {\n data[UNSAFE_DEFERRED_SYMBOL] = result.activeDeferreds[match.route.id];\n }\n\n return data;\n }\n\n return undefined;\n }\n\n async function queryImpl(request, location, matches, requestContext, routeMatch) {\n invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");\n\n try {\n if (isMutationMethod(request.method.toLowerCase())) {\n let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, routeMatch != null);\n return result;\n }\n\n let result = await loadRouteData(request, matches, requestContext, routeMatch);\n return isResponse(result) ? result : _extends({}, result, {\n actionData: null,\n actionHeaders: {}\n });\n } catch (e) {\n // If the user threw/returned a Response in callLoaderOrAction, we throw\n // it to bail out and then return or throw here based on whether the user\n // returned or threw\n if (isQueryRouteResponse(e)) {\n if (e.type === ResultType.error && !isRedirectResponse(e.response)) {\n throw e.response;\n }\n\n return e.response;\n } // Redirects are always returned since they don\'t propagate to catch\n // boundaries\n\n\n if (isRedirectResponse(e)) {\n return e;\n }\n\n throw e;\n }\n }\n\n async function submit(request, matches, actionMatch, requestContext, isRouteRequest) {\n let result;\n\n if (!actionMatch.route.action) {\n let error = getInternalRouterError(405, {\n method: request.method,\n pathname: new URL(request.url).pathname,\n routeId: actionMatch.route.id\n });\n\n if (isRouteRequest) {\n throw error;\n }\n\n result = {\n type: ResultType.error,\n error\n };\n } else {\n result = await callLoaderOrAction("action", request, actionMatch, matches, basename, true, isRouteRequest, requestContext);\n\n if (request.signal.aborted) {\n let method = isRouteRequest ? "queryRoute" : "query";\n throw new Error(method + "() call aborted");\n }\n }\n\n if (isRedirectResult(result)) {\n // Uhhhh - this should never happen, we should always throw these from\n // callLoaderOrAction, but the type narrowing here keeps TS happy and we\n // can get back on the "throw all redirect responses" train here should\n // this ever happen :/\n throw new Response(null, {\n status: result.status,\n headers: {\n Location: result.location\n }\n });\n }\n\n if (isDeferredResult(result)) {\n let error = getInternalRouterError(400, {\n type: "defer-action"\n });\n\n if (isRouteRequest) {\n throw error;\n }\n\n result = {\n type: ResultType.error,\n error\n };\n }\n\n if (isRouteRequest) {\n // Note: This should only be non-Response values if we get here, since\n // isRouteRequest should throw any Response received in callLoaderOrAction\n if (isErrorResult(result)) {\n throw result.error;\n }\n\n return {\n matches: [actionMatch],\n loaderData: {},\n actionData: {\n [actionMatch.route.id]: result.data\n },\n errors: null,\n // Note: statusCode + headers are unused here since queryRoute will\n // return the raw Response or value\n statusCode: 200,\n loaderHeaders: {},\n actionHeaders: {},\n activeDeferreds: null\n };\n }\n\n if (isErrorResult(result)) {\n // Store off the pending error - we use it to determine which loaders\n // to call and will commit it when we complete the navigation\n let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);\n let context = await loadRouteData(request, matches, requestContext, undefined, {\n [boundaryMatch.route.id]: result.error\n }); // action status codes take precedence over loader status codes\n\n return _extends({}, context, {\n statusCode: isRouteErrorResponse(result.error) ? result.error.status : 500,\n actionData: null,\n actionHeaders: _extends({}, result.headers ? {\n [actionMatch.route.id]: result.headers\n } : {})\n });\n } // Create a GET request for the loaders\n\n\n let loaderRequest = new Request(request.url, {\n headers: request.headers,\n redirect: request.redirect,\n signal: request.signal\n });\n let context = await loadRouteData(loaderRequest, matches, requestContext);\n return _extends({}, context, result.statusCode ? {\n statusCode: result.statusCode\n } : {}, {\n actionData: {\n [actionMatch.route.id]: result.data\n },\n actionHeaders: _extends({}, result.headers ? {\n [actionMatch.route.id]: result.headers\n } : {})\n });\n }\n\n async function loadRouteData(request, matches, requestContext, routeMatch, pendingActionError) {\n let isRouteRequest = routeMatch != null; // Short circuit if we have no loaders to run (queryRoute())\n\n if (isRouteRequest && !(routeMatch != null && routeMatch.route.loader)) {\n throw getInternalRouterError(400, {\n method: request.method,\n pathname: new URL(request.url).pathname,\n routeId: routeMatch == null ? void 0 : routeMatch.route.id\n });\n }\n\n let requestMatches = routeMatch ? [routeMatch] : getLoaderMatchesUntilBoundary(matches, Object.keys(pendingActionError || {})[0]);\n let matchesToLoad = requestMatches.filter(m => m.route.loader); // Short circuit if we have no loaders to run (query())\n\n if (matchesToLoad.length === 0) {\n return {\n matches,\n // Add a null for all matched routes for proper revalidation on the client\n loaderData: matches.reduce((acc, m) => Object.assign(acc, {\n [m.route.id]: null\n }), {}),\n errors: pendingActionError || null,\n statusCode: 200,\n loaderHeaders: {},\n activeDeferreds: null\n };\n }\n\n let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, basename, true, isRouteRequest, requestContext))]);\n\n if (request.signal.aborted) {\n let method = isRouteRequest ? "queryRoute" : "query";\n throw new Error(method + "() call aborted");\n } // Process and commit output from loaders\n\n\n let activeDeferreds = new Map();\n let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionError, activeDeferreds); // Add a null for any non-loader matches for proper revalidation on the client\n\n let executedLoaders = new Set(matchesToLoad.map(match => match.route.id));\n matches.forEach(match => {\n if (!executedLoaders.has(match.route.id)) {\n context.loaderData[match.route.id] = null;\n }\n });\n return _extends({}, context, {\n matches,\n activeDeferreds: activeDeferreds.size > 0 ? Object.fromEntries(activeDeferreds.entries()) : null\n });\n }\n\n return {\n dataRoutes,\n query,\n queryRoute\n };\n} //#endregion\n////////////////////////////////////////////////////////////////////////////////\n//#region Helpers\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * Given an existing StaticHandlerContext and an error thrown at render time,\n * provide an updated StaticHandlerContext suitable for a second SSR render\n */\n\nfunction getStaticContextFromError(routes, context, error) {\n let newContext = _extends({}, context, {\n statusCode: 500,\n errors: {\n [context._deepestRenderedBoundaryId || routes[0].id]: error\n }\n });\n\n return newContext;\n}\n\nfunction isSubmissionNavigation(opts) {\n return opts != null && "formData" in opts;\n} // Normalize navigation options by converting formMethod=GET formData objects to\n// URLSearchParams so they behave identically to links with query params\n\n\nfunction normalizeNavigateOptions(to, opts, isFetcher) {\n if (isFetcher === void 0) {\n isFetcher = false;\n }\n\n let path = typeof to === "string" ? to : createPath(to); // Return location verbatim on non-submission navigations\n\n if (!opts || !isSubmissionNavigation(opts)) {\n return {\n path\n };\n }\n\n if (opts.formMethod && !isValidMethod(opts.formMethod)) {\n return {\n path,\n error: getInternalRouterError(405, {\n method: opts.formMethod\n })\n };\n } // Create a Submission on non-GET navigations\n\n\n let submission;\n\n if (opts.formData) {\n submission = {\n formMethod: opts.formMethod || "get",\n formAction: stripHashFromPath(path),\n formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",\n formData: opts.formData\n };\n\n if (isMutationMethod(submission.formMethod)) {\n return {\n path,\n submission\n };\n }\n } // Flatten submission onto URLSearchParams for GET submissions\n\n\n let parsedPath = parsePath(path);\n let searchParams = convertFormDataToSearchParams(opts.formData); // Since fetcher GET submissions only run a single loader (as opposed to\n // navigation GET submissions which run all loaders), we need to preserve\n // any incoming ?index params\n\n if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {\n searchParams.append("index", "");\n }\n\n parsedPath.search = "?" + searchParams;\n return {\n path: createPath(parsedPath),\n submission\n };\n} // Filter out all routes below any caught error as they aren\'t going to\n// render so we don\'t need to load them\n\n\nfunction getLoaderMatchesUntilBoundary(matches, boundaryId) {\n let boundaryMatches = matches;\n\n if (boundaryId) {\n let index = matches.findIndex(m => m.route.id === boundaryId);\n\n if (index >= 0) {\n boundaryMatches = matches.slice(0, index);\n }\n }\n\n return boundaryMatches;\n}\n\nfunction getMatchesToLoad(history, state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, fetchLoadMatches, routesToUse, basename, pendingActionData, pendingError) {\n let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined;\n let currentUrl = history.createURL(state.location);\n let nextUrl = history.createURL(location);\n let defaultShouldRevalidate = // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate\n isRevalidationRequired || // Clicked the same link, resubmitted a GET form\n currentUrl.toString() === nextUrl.toString() || // Search params affect all loaders\n currentUrl.search !== nextUrl.search; // Pick navigation matches that are net-new or qualify for revalidation\n\n let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;\n let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);\n let navigationMatches = boundaryMatches.filter((match, index) => {\n if (match.route.loader == null) {\n return false;\n } // Always call the loader on new route instances and pending defer cancellations\n\n\n if (isNewLoader(state.loaderData, state.matches[index], match) || cancelledDeferredRoutes.some(id => id === match.route.id)) {\n return true;\n } // This is the default implementation for when we revalidate. If the route\n // provides it\'s own implementation, then we give them full control but\n // provide this value so they can leverage it if needed after they check\n // their own specific use cases\n\n\n let currentRouteMatch = state.matches[index];\n let nextRouteMatch = match;\n return shouldRevalidateLoader(match, _extends({\n currentUrl,\n currentParams: currentRouteMatch.params,\n nextUrl,\n nextParams: nextRouteMatch.params\n }, submission, {\n actionResult,\n defaultShouldRevalidate: defaultShouldRevalidate || isNewRouteInstance(currentRouteMatch, nextRouteMatch)\n }));\n }); // Pick fetcher.loads that need to be revalidated\n\n let revalidatingFetchers = [];\n fetchLoadMatches.forEach((f, key) => {\n // Don\'t revalidate if fetcher won\'t be present in the subsequent render\n if (!matches.some(m => m.route.id === f.routeId)) {\n return;\n }\n\n let fetcherMatches = matchRoutes(routesToUse, f.path, basename); // If the fetcher path no longer matches, push it in with null matches so\n // we can trigger a 404 in callLoadersAndMaybeResolveData\n\n if (!fetcherMatches) {\n revalidatingFetchers.push(_extends({\n key\n }, f, {\n matches: null,\n match: null\n }));\n return;\n }\n\n let fetcherMatch = getTargetMatch(fetcherMatches, f.path);\n\n if (cancelledFetcherLoads.includes(key)) {\n revalidatingFetchers.push(_extends({\n key,\n matches: fetcherMatches,\n match: fetcherMatch\n }, f));\n return;\n } // Revalidating fetchers are decoupled from the route matches since they\n // hit a static href, so they _always_ check shouldRevalidate and the\n // default is strictly if a revalidation is explicitly required (action\n // submissions, useRevalidator, X-Remix-Revalidate).\n\n\n let shouldRevalidate = shouldRevalidateLoader(fetcherMatch, _extends({\n currentUrl,\n currentParams: state.matches[state.matches.length - 1].params,\n nextUrl,\n nextParams: matches[matches.length - 1].params\n }, submission, {\n actionResult,\n defaultShouldRevalidate\n }));\n\n if (shouldRevalidate) {\n revalidatingFetchers.push(_extends({\n key,\n matches: fetcherMatches,\n match: fetcherMatch\n }, f));\n }\n });\n return [navigationMatches, revalidatingFetchers];\n}\n\nfunction isNewLoader(currentLoaderData, currentMatch, match) {\n let isNew = // [a] -> [a, b]\n !currentMatch || // [a, b] -> [a, c]\n match.route.id !== currentMatch.route.id; // Handle the case that we don\'t have data for a re-used route, potentially\n // from a prior error or from a cancelled pending deferred\n\n let isMissingData = currentLoaderData[match.route.id] === undefined; // Always load if this is a net-new route or we don\'t yet have data\n\n return isNew || isMissingData;\n}\n\nfunction isNewRouteInstance(currentMatch, match) {\n let currentPath = currentMatch.route.path;\n return (// param change for this match, /users/123 -> /users/456\n currentMatch.pathname !== match.pathname || // splat param changed, which is not present in match.path\n // e.g. /files/images/avatar.jpg -> files/finances.xls\n currentPath != null && currentPath.endsWith("*") && currentMatch.params["*"] !== match.params["*"]\n );\n}\n\nfunction shouldRevalidateLoader(loaderMatch, arg) {\n if (loaderMatch.route.shouldRevalidate) {\n let routeChoice = loaderMatch.route.shouldRevalidate(arg);\n\n if (typeof routeChoice === "boolean") {\n return routeChoice;\n }\n }\n\n return arg.defaultShouldRevalidate;\n}\n\nasync function callLoaderOrAction(type, request, match, matches, basename, isStaticRequest, isRouteRequest, requestContext) {\n if (basename === void 0) {\n basename = "/";\n }\n\n if (isStaticRequest === void 0) {\n isStaticRequest = false;\n }\n\n if (isRouteRequest === void 0) {\n isRouteRequest = false;\n }\n\n let resultType;\n let result; // Setup a promise we can race against so that abort signals short circuit\n\n let reject;\n let abortPromise = new Promise((_, r) => reject = r);\n\n let onReject = () => reject();\n\n request.signal.addEventListener("abort", onReject);\n\n try {\n let handler = match.route[type];\n invariant(handler, "Could not find the " + type + " to run on the \\"" + match.route.id + "\\" route");\n result = await Promise.race([handler({\n request,\n params: match.params,\n context: requestContext\n }), abortPromise]);\n invariant(result !== undefined, "You defined " + (type === "action" ? "an action" : "a loader") + " for route " + ("\\"" + match.route.id + "\\" but didn\'t return anything from your `" + type + "` ") + "function. Please return a value or `null`.");\n } catch (e) {\n resultType = ResultType.error;\n result = e;\n } finally {\n request.signal.removeEventListener("abort", onReject);\n }\n\n if (isResponse(result)) {\n let status = result.status; // Process redirects\n\n if (redirectStatusCodes.has(status)) {\n let location = result.headers.get("Location");\n invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header"); // Support relative routing in internal redirects\n\n if (!ABSOLUTE_URL_REGEX.test(location)) {\n let activeMatches = matches.slice(0, matches.indexOf(match) + 1);\n let routePathnames = getPathContributingMatches(activeMatches).map(match => match.pathnameBase);\n let resolvedLocation = resolveTo(location, routePathnames, new URL(request.url).pathname);\n invariant(createPath(resolvedLocation), "Unable to resolve redirect location: " + location); // Prepend the basename to the redirect location if we have one\n\n if (basename) {\n let path = resolvedLocation.pathname;\n resolvedLocation.pathname = path === "/" ? basename : joinPaths([basename, path]);\n }\n\n location = createPath(resolvedLocation);\n } else if (!isStaticRequest) {\n // Strip off the protocol+origin for same-origin + same-basename absolute\n // redirects. If this is a static request, we can let it go back to the\n // browser as-is\n let currentUrl = new URL(request.url);\n let url = location.startsWith("//") ? new URL(currentUrl.protocol + location) : new URL(location);\n let isSameBasename = stripBasename(url.pathname, basename) != null;\n\n if (url.origin === currentUrl.origin && isSameBasename) {\n location = url.pathname + url.search + url.hash;\n }\n } // Don\'t process redirects in the router during static requests requests.\n // Instead, throw the Response and let the server handle it with an HTTP\n // redirect. We also update the Location header in place in this flow so\n // basename and relative routing is taken into account\n\n\n if (isStaticRequest) {\n result.headers.set("Location", location);\n throw result;\n }\n\n return {\n type: ResultType.redirect,\n status,\n location,\n revalidate: result.headers.get("X-Remix-Revalidate") !== null\n };\n } // For SSR single-route requests, we want to hand Responses back directly\n // without unwrapping. We do this with the QueryRouteResponse wrapper\n // interface so we can know whether it was returned or thrown\n\n\n if (isRouteRequest) {\n // eslint-disable-next-line no-throw-literal\n throw {\n type: resultType || ResultType.data,\n response: result\n };\n }\n\n let data;\n let contentType = result.headers.get("Content-Type"); // Check between word boundaries instead of startsWith() due to the last\n // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type\n\n if (contentType && /\\bapplication\\/json\\b/.test(contentType)) {\n data = await result.json();\n } else {\n data = await result.text();\n }\n\n if (resultType === ResultType.error) {\n return {\n type: resultType,\n error: new ErrorResponse(status, result.statusText, data),\n headers: result.headers\n };\n }\n\n return {\n type: ResultType.data,\n data,\n statusCode: result.status,\n headers: result.headers\n };\n }\n\n if (resultType === ResultType.error) {\n return {\n type: resultType,\n error: result\n };\n }\n\n if (result instanceof DeferredData) {\n var _result$init, _result$init2;\n\n return {\n type: ResultType.deferred,\n deferredData: result,\n statusCode: (_result$init = result.init) == null ? void 0 : _result$init.status,\n headers: ((_result$init2 = result.init) == null ? void 0 : _result$init2.headers) && new Headers(result.init.headers)\n };\n }\n\n return {\n type: ResultType.data,\n data: result\n };\n} // Utility method for creating the Request instances for loaders/actions during\n// client-side navigations and fetches. During SSR we will always have a\n// Request instance from the static handler (query/queryRoute)\n\n\nfunction createClientSideRequest(history, location, signal, submission) {\n let url = history.createURL(stripHashFromPath(location)).toString();\n let init = {\n signal\n };\n\n if (submission && isMutationMethod(submission.formMethod)) {\n let {\n formMethod,\n formEncType,\n formData\n } = submission;\n init.method = formMethod.toUpperCase();\n init.body = formEncType === "application/x-www-form-urlencoded" ? convertFormDataToSearchParams(formData) : formData;\n } // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)\n\n\n return new Request(url, init);\n}\n\nfunction convertFormDataToSearchParams(formData) {\n let searchParams = new URLSearchParams();\n\n for (let [key, value] of formData.entries()) {\n // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs\n searchParams.append(key, value instanceof File ? value.name : value);\n }\n\n return searchParams;\n}\n\nfunction processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds) {\n // Fill in loaderData/errors from our loaders\n let loaderData = {};\n let errors = null;\n let statusCode;\n let foundError = false;\n let loaderHeaders = {}; // Process loader results into state.loaderData/state.errors\n\n results.forEach((result, index) => {\n let id = matchesToLoad[index].route.id;\n invariant(!isRedirectResult(result), "Cannot handle redirect results in processLoaderData");\n\n if (isErrorResult(result)) {\n // Look upwards from the matched route for the closest ancestor\n // error boundary, defaulting to the root match\n let boundaryMatch = findNearestBoundary(matches, id);\n let error = result.error; // If we have a pending action error, we report it at the highest-route\n // that throws a loader error, and then clear it out to indicate that\n // it was consumed\n\n if (pendingError) {\n error = Object.values(pendingError)[0];\n pendingError = undefined;\n }\n\n errors = errors || {}; // Prefer higher error values if lower errors bubble to the same boundary\n\n if (errors[boundaryMatch.route.id] == null) {\n errors[boundaryMatch.route.id] = error;\n } // Clear our any prior loaderData for the throwing route\n\n\n loaderData[id] = undefined; // Once we find our first (highest) error, we set the status code and\n // prevent deeper status codes from overriding\n\n if (!foundError) {\n foundError = true;\n statusCode = isRouteErrorResponse(result.error) ? result.error.status : 500;\n }\n\n if (result.headers) {\n loaderHeaders[id] = result.headers;\n }\n } else {\n if (isDeferredResult(result)) {\n activeDeferreds.set(id, result.deferredData);\n loaderData[id] = result.deferredData.data;\n } else {\n loaderData[id] = result.data;\n } // Error status codes always override success status codes, but if all\n // loaders are successful we take the deepest status code.\n\n\n if (result.statusCode != null && result.statusCode !== 200 && !foundError) {\n statusCode = result.statusCode;\n }\n\n if (result.headers) {\n loaderHeaders[id] = result.headers;\n }\n }\n }); // If we didn\'t consume the pending action error (i.e., all loaders\n // resolved), then consume it here. Also clear out any loaderData for the\n // throwing route\n\n if (pendingError) {\n errors = pendingError;\n loaderData[Object.keys(pendingError)[0]] = undefined;\n }\n\n return {\n loaderData,\n errors,\n statusCode: statusCode || 200,\n loaderHeaders\n };\n}\n\nfunction processLoaderData(state, matches, matchesToLoad, results, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds) {\n let {\n loaderData,\n errors\n } = processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds); // Process results from our revalidating fetchers\n\n for (let index = 0; index < revalidatingFetchers.length; index++) {\n let {\n key,\n match\n } = revalidatingFetchers[index];\n invariant(fetcherResults !== undefined && fetcherResults[index] !== undefined, "Did not find corresponding fetcher result");\n let result = fetcherResults[index]; // Process fetcher non-redirect errors\n\n if (isErrorResult(result)) {\n let boundaryMatch = findNearestBoundary(state.matches, match == null ? void 0 : match.route.id);\n\n if (!(errors && errors[boundaryMatch.route.id])) {\n errors = _extends({}, errors, {\n [boundaryMatch.route.id]: result.error\n });\n }\n\n state.fetchers.delete(key);\n } else if (isRedirectResult(result)) {\n // Should never get here, redirects should get processed above, but we\n // keep this to type narrow to a success result in the else\n invariant(false, "Unhandled fetcher revalidation redirect");\n } else if (isDeferredResult(result)) {\n // Should never get here, deferred data should be awaited for fetchers\n // in resolveDeferredResults\n invariant(false, "Unhandled fetcher deferred data");\n } else {\n let doneFetcher = {\n state: "idle",\n data: result.data,\n formMethod: undefined,\n formAction: undefined,\n formEncType: undefined,\n formData: undefined,\n " _hasFetcherDoneAnything ": true\n };\n state.fetchers.set(key, doneFetcher);\n }\n }\n\n return {\n loaderData,\n errors\n };\n}\n\nfunction mergeLoaderData(loaderData, newLoaderData, matches, errors) {\n let mergedLoaderData = _extends({}, newLoaderData);\n\n for (let match of matches) {\n let id = match.route.id;\n\n if (newLoaderData.hasOwnProperty(id)) {\n if (newLoaderData[id] !== undefined) {\n mergedLoaderData[id] = newLoaderData[id];\n }\n } else if (loaderData[id] !== undefined && match.route.loader) {\n // Preserve existing keys not included in newLoaderData and where a loader\n // wasn\'t removed by HMR\n mergedLoaderData[id] = loaderData[id];\n }\n\n if (errors && errors.hasOwnProperty(id)) {\n // Don\'t keep any loader data below the boundary\n break;\n }\n }\n\n return mergedLoaderData;\n} // Find the nearest error boundary, looking upwards from the leaf route (or the\n// route specified by routeId) for the closest ancestor error boundary,\n// defaulting to the root match\n\n\nfunction findNearestBoundary(matches, routeId) {\n let eligibleMatches = routeId ? matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1) : [...matches];\n return eligibleMatches.reverse().find(m => m.route.hasErrorBoundary === true) || matches[0];\n}\n\nfunction getShortCircuitMatches(routes) {\n // Prefer a root layout route if present, otherwise shim in a route object\n let route = routes.find(r => r.index || !r.path || r.path === "/") || {\n id: "__shim-error-route__"\n };\n return {\n matches: [{\n params: {},\n pathname: "",\n pathnameBase: "",\n route\n }],\n route\n };\n}\n\nfunction getInternalRouterError(status, _temp4) {\n let {\n pathname,\n routeId,\n method,\n type\n } = _temp4 === void 0 ? {} : _temp4;\n let statusText = "Unknown Server Error";\n let errorMessage = "Unknown @remix-run/router error";\n\n if (status === 400) {\n statusText = "Bad Request";\n\n if (method && pathname && routeId) {\n errorMessage = "You made a " + method + " request to \\"" + pathname + "\\" but " + ("did not provide a `loader` for route \\"" + routeId + "\\", ") + "so there is no way to handle the request.";\n } else if (type === "defer-action") {\n errorMessage = "defer() is not supported in actions";\n }\n } else if (status === 403) {\n statusText = "Forbidden";\n errorMessage = "Route \\"" + routeId + "\\" does not match URL \\"" + pathname + "\\"";\n } else if (status === 404) {\n statusText = "Not Found";\n errorMessage = "No route matches URL \\"" + pathname + "\\"";\n } else if (status === 405) {\n statusText = "Method Not Allowed";\n\n if (method && pathname && routeId) {\n errorMessage = "You made a " + method.toUpperCase() + " request to \\"" + pathname + "\\" but " + ("did not provide an `action` for route \\"" + routeId + "\\", ") + "so there is no way to handle the request.";\n } else if (method) {\n errorMessage = "Invalid request method \\"" + method.toUpperCase() + "\\"";\n }\n }\n\n return new ErrorResponse(status || 500, statusText, new Error(errorMessage), true);\n} // Find any returned redirect errors, starting from the lowest match\n\n\nfunction findRedirect(results) {\n for (let i = results.length - 1; i >= 0; i--) {\n let result = results[i];\n\n if (isRedirectResult(result)) {\n return result;\n }\n }\n}\n\nfunction stripHashFromPath(path) {\n let parsedPath = typeof path === "string" ? parsePath(path) : path;\n return createPath(_extends({}, parsedPath, {\n hash: ""\n }));\n}\n\nfunction isHashChangeOnly(a, b) {\n return a.pathname === b.pathname && a.search === b.search && a.hash !== b.hash;\n}\n\nfunction isDeferredResult(result) {\n return result.type === ResultType.deferred;\n}\n\nfunction isErrorResult(result) {\n return result.type === ResultType.error;\n}\n\nfunction isRedirectResult(result) {\n return (result && result.type) === ResultType.redirect;\n}\n\nfunction isResponse(value) {\n return value != null && typeof value.status === "number" && typeof value.statusText === "string" && typeof value.headers === "object" && typeof value.body !== "undefined";\n}\n\nfunction isRedirectResponse(result) {\n if (!isResponse(result)) {\n return false;\n }\n\n let status = result.status;\n let location = result.headers.get("Location");\n return status >= 300 && status <= 399 && location != null;\n}\n\nfunction isQueryRouteResponse(obj) {\n return obj && isResponse(obj.response) && (obj.type === ResultType.data || ResultType.error);\n}\n\nfunction isValidMethod(method) {\n return validRequestMethods.has(method);\n}\n\nfunction isMutationMethod(method) {\n return validMutationMethods.has(method);\n}\n\nasync function resolveDeferredResults(currentMatches, matchesToLoad, results, signal, isFetcher, currentLoaderData) {\n for (let index = 0; index < results.length; index++) {\n let result = results[index];\n let match = matchesToLoad[index]; // If we don\'t have a match, then we can have a deferred result to do\n // anything with. This is for revalidating fetchers where the route was\n // removed during HMR\n\n if (!match) {\n continue;\n }\n\n let currentMatch = currentMatches.find(m => m.route.id === match.route.id);\n let isRevalidatingLoader = currentMatch != null && !isNewRouteInstance(currentMatch, match) && (currentLoaderData && currentLoaderData[match.route.id]) !== undefined;\n\n if (isDeferredResult(result) && (isFetcher || isRevalidatingLoader)) {\n // Note: we do not have to touch activeDeferreds here since we race them\n // against the signal in resolveDeferredData and they\'ll get aborted\n // there if needed\n await resolveDeferredData(result, signal, isFetcher).then(result => {\n if (result) {\n results[index] = result || results[index];\n }\n });\n }\n }\n}\n\nasync function resolveDeferredData(result, signal, unwrap) {\n if (unwrap === void 0) {\n unwrap = false;\n }\n\n let aborted = await result.deferredData.resolveData(signal);\n\n if (aborted) {\n return;\n }\n\n if (unwrap) {\n try {\n return {\n type: ResultType.data,\n data: result.deferredData.unwrappedData\n };\n } catch (e) {\n // Handle any TrackedPromise._error values encountered while unwrapping\n return {\n type: ResultType.error,\n error: e\n };\n }\n }\n\n return {\n type: ResultType.data,\n data: result.deferredData.data\n };\n}\n\nfunction hasNakedIndexQuery(search) {\n return new URLSearchParams(search).getAll("index").some(v => v === "");\n} // Note: This should match the format exported by useMatches, so if you change\n// this please also change that :) Eventually we\'ll DRY this up\n\n\nfunction createUseMatchesMatch(match, loaderData) {\n let {\n route,\n pathname,\n params\n } = match;\n return {\n id: route.id,\n pathname,\n params,\n data: loaderData[route.id],\n handle: route.handle\n };\n}\n\nfunction getTargetMatch(matches, location) {\n let search = typeof location === "string" ? parsePath(location).search : location.search;\n\n if (matches[matches.length - 1].route.index && hasNakedIndexQuery(search || "")) {\n // Return the leaf index route when index is present\n return matches[matches.length - 1];\n } // Otherwise grab the deepest "path contributing" match (ignoring index and\n // pathless layout routes)\n\n\n let pathMatches = getPathContributingMatches(matches);\n return pathMatches[pathMatches.length - 1];\n} //#endregion\n\n\n//# sourceMappingURL=router.js.map\n\n\n//# sourceURL=webpack://frontend/./node_modules/@remix-run/router/dist/router.js?')},"./src/App.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{"use strict";eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "./node_modules/react/index.js");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var react_router_dom__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! react-router-dom */ "./node_modules/react-router-dom/dist/index.js");\n/* harmony import */ var react_router_dom__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! react-router-dom */ "./node_modules/react-router/dist/index.js");\n/* harmony import */ var react_loader_spinner__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-loader-spinner */ "./node_modules/react-loader-spinner/dist/esm/index.js");\n/* harmony import */ var _views_login__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./views/login */ "./src/views/login.js");\n\n\n\n\nconst Dashboard = /*#__PURE__*/(0,react__WEBPACK_IMPORTED_MODULE_0__.lazy)(() => __webpack_require__.e(/*! import() */ "src_views_dashboard_js").then(__webpack_require__.bind(__webpack_require__, /*! ./views/dashboard */ "./src/views/dashboard.js")));\nfunction App() {\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_router_dom__WEBPACK_IMPORTED_MODULE_3__.BrowserRouter, null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react__WEBPACK_IMPORTED_MODULE_0__.Suspense, {\n fallback: /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "loading-screen-overlay",\n id: "loading-overlay"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "loading-screen"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_loader_spinner__WEBPACK_IMPORTED_MODULE_1__.ColorRing, {\n visible: true,\n height: "80",\n width: "80",\n ariaLabel: "blocks-loading",\n wrapperStyle: {},\n wrapperClass: "blocks-wrapper",\n colors: [\'#404e67\', \'#01a9ac\', \'#64c5b1\', \'#1ABB9C\']\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "Data wordt geladen...")))\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_router_dom__WEBPACK_IMPORTED_MODULE_4__.Routes, null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_router_dom__WEBPACK_IMPORTED_MODULE_4__.Route, {\n exact: true,\n path: "/",\n element: localStorage.getItem(\'token\') ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(Dashboard, null) : /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_views_login__WEBPACK_IMPORTED_MODULE_2__["default"], null)\n }))));\n}\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (App);\n\n//# sourceURL=webpack://frontend/./src/App.js?')},"./src/index.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{"use strict";eval('__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "./node_modules/react/index.js");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var react_dom_client__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-dom/client */ "./node_modules/react-dom/client.js");\n/* harmony import */ var _App__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./App */ "./src/App.js");\n\n\n\nconst root = react_dom_client__WEBPACK_IMPORTED_MODULE_1__.createRoot(document.getElementById(\'app\'));\nroot.render( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_App__WEBPACK_IMPORTED_MODULE_2__["default"], null)));\n\n//# sourceURL=webpack://frontend/./src/index.js?')},"./src/views/login.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{"use strict";eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "./node_modules/react/index.js");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n\nconst Login = props => {\n const [username, setUsername] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(\'\');\n const [password, setPassword] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(\'\');\n const [loggedin, setLoggedin] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);\n const getToken = idToken => {\n localStorage.getItem(\'token\');\n };\n const setToken = idToken => {\n localStorage.setItem(\'token\', idToken);\n };\n const isTokenExpired = token => {\n try {\n const decoded = decode(token);\n if (decoded.exp < Date.now() / 1000) {\n return true;\n } else return false;\n } catch (err) {\n return false;\n }\n };\n const login = async (username, password) => {\n const formData = new FormData();\n formData.append(\'username\', username);\n formData.append(\'password\', password);\n return authFetch(`http://localhost:8000/api/auth/login?username=${username}&password=${password}`, {\n method: \'POST\',\n body: formData\n }).then(res => {\n console.log(res.user);\n if (res.user) {\n localStorage.setItem(\'id\', res.user.id);\n localStorage.setItem(\'name\', res.user.name);\n localStorage.setItem(\'username\', res.user.username);\n localStorage.setItem(\'email\', res.user.email);\n setToken(res.token);\n return Promise.resolve(res);\n }\n });\n };\n const handleFormSubmit = async event => {\n event.preventDefault();\n const res = await login(username, password);\n console.log(res);\n if (res.user.id) {\n setLoggedin(true);\n window.location.href = "/";\n } else {\n alert("Inlogegevens zijn onjuist. Probeer het opnieuw");\n }\n };\n const handleChange = event => {\n if (event.target.name === \'username\') {\n setUsername(event.target.value);\n } else {\n setPassword(event.target.value);\n }\n };\n const loggedIn = () => {\n const token = getToken();\n return !!token && !isTokenExpired(token);\n };\n const authFetch = (url, options) => {\n if (loggedIn()) {\n headers[\'Authorization\'] = \'Bearer \' + undefined.getToken();\n }\n return fetch(url, options).then(_checkStatus).then(response => response.json());\n };\n const _checkStatus = response => {\n if (response.status >= 200 && response.status < 300) {\n return response;\n } else {\n var error = new Error(response.statusText);\n error.response = response;\n throw error;\n }\n };\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "login_overlay"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "login"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("img", {\n className: "logo_text",\n src: "/static/images/logo.png"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("form", {\n onSubmit: event => handleFormSubmit(event)\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "mb-3"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "icon"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {\n className: "fas fa-user"\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", {\n type: "text",\n onChange: handleChange,\n className: "form-control",\n name: "username",\n id: "username",\n placeholder: "Gebruikersnaam",\n "aria-describedby": "emailHelp"\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "mb-3"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "icon"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {\n className: "fas fa-key"\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", {\n type: "password",\n onChange: handleChange,\n className: "form-control",\n name: "password",\n id: "password",\n placeholder: "Wachtwoord"\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", {\n type: "submit",\n className: "btn btn-primary"\n }, "Inloggen"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n id: "err_msg"\n }))));\n};\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Login);\n\n//# sourceURL=webpack://frontend/./src/views/login.js?')},"./node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js":(module,__unused_webpack_exports,__webpack_require__)=>{"use strict";eval("\n\nvar reactIs = __webpack_require__(/*! react-is */ \"./node_modules/react-is/index.js\");\n\n/**\n * Copyright 2015, Yahoo! Inc.\n * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.\n */\nvar REACT_STATICS = {\n childContextTypes: true,\n contextType: true,\n contextTypes: true,\n defaultProps: true,\n displayName: true,\n getDefaultProps: true,\n getDerivedStateFromError: true,\n getDerivedStateFromProps: true,\n mixins: true,\n propTypes: true,\n type: true\n};\nvar KNOWN_STATICS = {\n name: true,\n length: true,\n prototype: true,\n caller: true,\n callee: true,\n arguments: true,\n arity: true\n};\nvar FORWARD_REF_STATICS = {\n '$$typeof': true,\n render: true,\n defaultProps: true,\n displayName: true,\n propTypes: true\n};\nvar MEMO_STATICS = {\n '$$typeof': true,\n compare: true,\n defaultProps: true,\n displayName: true,\n propTypes: true,\n type: true\n};\nvar TYPE_STATICS = {};\nTYPE_STATICS[reactIs.ForwardRef] = FORWARD_REF_STATICS;\nTYPE_STATICS[reactIs.Memo] = MEMO_STATICS;\n\nfunction getStatics(component) {\n // React v16.11 and below\n if (reactIs.isMemo(component)) {\n return MEMO_STATICS;\n } // React v16.12 and above\n\n\n return TYPE_STATICS[component['$$typeof']] || REACT_STATICS;\n}\n\nvar defineProperty = Object.defineProperty;\nvar getOwnPropertyNames = Object.getOwnPropertyNames;\nvar getOwnPropertySymbols = Object.getOwnPropertySymbols;\nvar getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;\nvar getPrototypeOf = Object.getPrototypeOf;\nvar objectPrototype = Object.prototype;\nfunction hoistNonReactStatics(targetComponent, sourceComponent, blacklist) {\n if (typeof sourceComponent !== 'string') {\n // don't hoist over string (html) components\n if (objectPrototype) {\n var inheritedComponent = getPrototypeOf(sourceComponent);\n\n if (inheritedComponent && inheritedComponent !== objectPrototype) {\n hoistNonReactStatics(targetComponent, inheritedComponent, blacklist);\n }\n }\n\n var keys = getOwnPropertyNames(sourceComponent);\n\n if (getOwnPropertySymbols) {\n keys = keys.concat(getOwnPropertySymbols(sourceComponent));\n }\n\n var targetStatics = getStatics(targetComponent);\n var sourceStatics = getStatics(sourceComponent);\n\n for (var i = 0; i < keys.length; ++i) {\n var key = keys[i];\n\n if (!KNOWN_STATICS[key] && !(blacklist && blacklist[key]) && !(sourceStatics && sourceStatics[key]) && !(targetStatics && targetStatics[key])) {\n var descriptor = getOwnPropertyDescriptor(sourceComponent, key);\n\n try {\n // Avoid failures from read-only properties\n defineProperty(targetComponent, key, descriptor);\n } catch (e) {}\n }\n }\n }\n\n return targetComponent;\n}\n\nmodule.exports = hoistNonReactStatics;\n\n\n//# sourceURL=webpack://frontend/./node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js?")},"./node_modules/react-dom/cjs/react-dom.development.js":(__unused_webpack_module,exports,__webpack_require__)=>{"use strict";eval("/**\n * @license React\n * react-dom.development.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\n\nif (true) {\n (function() {\n\n 'use strict';\n\n/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */\nif (\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart ===\n 'function'\n) {\n __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());\n}\n var React = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\nvar Scheduler = __webpack_require__(/*! scheduler */ \"./node_modules/scheduler/index.js\");\n\nvar ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n\nvar suppressWarning = false;\nfunction setSuppressWarning(newSuppressWarning) {\n {\n suppressWarning = newSuppressWarning;\n }\n} // In DEV, calls to console.warn and console.error get replaced\n// by calls to these methods by a Babel plugin.\n//\n// In PROD (or in packages without access to React internals),\n// they are left as they are instead.\n\nfunction warn(format) {\n {\n if (!suppressWarning) {\n for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n args[_key - 1] = arguments[_key];\n }\n\n printWarning('warn', format, args);\n }\n }\n}\nfunction error(format) {\n {\n if (!suppressWarning) {\n for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\n args[_key2 - 1] = arguments[_key2];\n }\n\n printWarning('error', format, args);\n }\n }\n}\n\nfunction printWarning(level, format, args) {\n // When changing this logic, you might want to also\n // update consoleWithStackDev.www.js as well.\n {\n var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;\n var stack = ReactDebugCurrentFrame.getStackAddendum();\n\n if (stack !== '') {\n format += '%s';\n args = args.concat([stack]);\n } // eslint-disable-next-line react-internal/safe-string-coercion\n\n\n var argsWithFormat = args.map(function (item) {\n return String(item);\n }); // Careful: RN currently depends on this prefix\n\n argsWithFormat.unshift('Warning: ' + format); // We intentionally don't use spread (or .apply) directly because it\n // breaks IE9: https://github.com/facebook/react/issues/13610\n // eslint-disable-next-line react-internal/no-production-logging\n\n Function.prototype.apply.call(console[level], console, argsWithFormat);\n }\n}\n\nvar FunctionComponent = 0;\nvar ClassComponent = 1;\nvar IndeterminateComponent = 2; // Before we know whether it is function or class\n\nvar HostRoot = 3; // Root of a host tree. Could be nested inside another node.\n\nvar HostPortal = 4; // A subtree. Could be an entry point to a different renderer.\n\nvar HostComponent = 5;\nvar HostText = 6;\nvar Fragment = 7;\nvar Mode = 8;\nvar ContextConsumer = 9;\nvar ContextProvider = 10;\nvar ForwardRef = 11;\nvar Profiler = 12;\nvar SuspenseComponent = 13;\nvar MemoComponent = 14;\nvar SimpleMemoComponent = 15;\nvar LazyComponent = 16;\nvar IncompleteClassComponent = 17;\nvar DehydratedFragment = 18;\nvar SuspenseListComponent = 19;\nvar ScopeComponent = 21;\nvar OffscreenComponent = 22;\nvar LegacyHiddenComponent = 23;\nvar CacheComponent = 24;\nvar TracingMarkerComponent = 25;\n\n// -----------------------------------------------------------------------------\n\nvar enableClientRenderFallbackOnTextMismatch = true; // TODO: Need to review this code one more time before landing\n// the react-reconciler package.\n\nvar enableNewReconciler = false; // Support legacy Primer support on internal FB www\n\nvar enableLazyContextPropagation = false; // FB-only usage. The new API has different semantics.\n\nvar enableLegacyHidden = false; // Enables unstable_avoidThisFallback feature in Fiber\n\nvar enableSuspenseAvoidThisFallback = false; // Enables unstable_avoidThisFallback feature in Fizz\n// React DOM Chopping Block\n//\n// Similar to main Chopping Block but only flags related to React DOM. These are\n// grouped because we will likely batch all of them into a single major release.\n// -----------------------------------------------------------------------------\n// Disable support for comment nodes as React DOM containers. Already disabled\n// in open source, but www codebase still relies on it. Need to remove.\n\nvar disableCommentsAsDOMContainers = true; // Disable javascript: URL strings in href for XSS protection.\n// and client rendering, mostly to allow JSX attributes to apply to the custom\n// element's object properties instead of only HTML attributes.\n// https://github.com/facebook/react/issues/11347\n\nvar enableCustomElementPropertySupport = false; // Disables children for