diff --git a/VocaDbWeb/Controllers/ReactController.cs b/VocaDbWeb/Controllers/ReactController.cs
new file mode 100644
index 0000000000..8ae74c7ec9
--- /dev/null
+++ b/VocaDbWeb/Controllers/ReactController.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace VocaDb.Web.Controllers
+{
+ public class ReactController : Controller
+ {
+ public IActionResult Index()
+ {
+ return View();
+ }
+ }
+}
diff --git a/VocaDbWeb/Scripts/App.tsx b/VocaDbWeb/Scripts/App.tsx
new file mode 100644
index 0000000000..cb716661e7
--- /dev/null
+++ b/VocaDbWeb/Scripts/App.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+import './i18n';
+
+const App = (): React.ReactElement => {
+ return <>>;
+};
+
+export default App;
diff --git a/VocaDbWeb/Scripts/i18n.ts b/VocaDbWeb/Scripts/i18n.ts
new file mode 100644
index 0000000000..6e9987eee9
--- /dev/null
+++ b/VocaDbWeb/Scripts/i18n.ts
@@ -0,0 +1,21 @@
+import i18n from 'i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+import Backend from 'i18next-http-backend';
+import { initReactI18next } from 'react-i18next';
+
+i18n
+ .use(Backend)
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ load: 'languageOnly',
+ fallbackLng: 'en',
+
+ interpolation: {
+ escapeValue: false,
+ },
+
+ react: {
+ useSuspense: false,
+ },
+ });
diff --git a/VocaDbWeb/Scripts/index.tsx b/VocaDbWeb/Scripts/index.tsx
new file mode 100644
index 0000000000..30b5d776ad
--- /dev/null
+++ b/VocaDbWeb/Scripts/index.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import App from './App';
+
+const app = document.getElementById('app');
+
+ReactDOM.render(
+
+
+ ,
+ app,
+);
diff --git a/VocaDbWeb/Startup.cs b/VocaDbWeb/Startup.cs
index eb8d6f3f71..f8b2500d57 100644
--- a/VocaDbWeb/Startup.cs
+++ b/VocaDbWeb/Startup.cs
@@ -370,6 +370,8 @@ void HandleHttpError(int code, string? description = null, string? msg = null)
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
+
+ endpoints.MapFallbackToController(action: "Index", controller: "React");
});
}
}
diff --git a/VocaDbWeb/Views/React/Index.cshtml b/VocaDbWeb/Views/React/Index.cshtml
new file mode 100644
index 0000000000..4422515b0e
--- /dev/null
+++ b/VocaDbWeb/Views/React/Index.cshtml
@@ -0,0 +1,43 @@
+@using System.Globalization
+@using VocaDb.Model.DataContracts.Users
+@using VocaDb.Model.Domain.Globalization
+@using VocaDb.Model.Helpers
+@using VocaDb.Model.Utils
+@using VocaDb.Web.Models.Shared
+@inherits VocaDbPage
+@addTagHelper *, VocaDb.ReMikus
+
+@{
+ Layout = null;
+
+ var stylesheet = Login.IsLoggedIn && !string.IsNullOrEmpty(Login.User.Stylesheet) ? Login.User.Stylesheet : Config.SiteSettings.DefaultStylesheet;
+}
+
+
+
+
+
+
+ Vocaloid Database
+
+
+
+ @if (!string.IsNullOrEmpty(stylesheet))
+ {
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/VocaDbWeb/tsconfig.json b/VocaDbWeb/tsconfig.json
index 99023ee011..8353af3e44 100644
--- a/VocaDbWeb/tsconfig.json
+++ b/VocaDbWeb/tsconfig.json
@@ -11,6 +11,7 @@
"forceConsistentCasingInFileNames": true,
"baseUrl": "./",
"paths": {
+ "@Components/*": ["Scripts/Components/*"],
"@DataContracts/*": ["Scripts/DataContracts/*"],
"@Helpers/*": ["Scripts/Helpers/*"],
"@KnockoutExtensions/*": ["Scripts/KnockoutExtensions/*"],
diff --git a/VocaDbWeb/webpack.mix.js b/VocaDbWeb/webpack.mix.js
index 36de509c49..9be365d31e 100644
--- a/VocaDbWeb/webpack.mix.js
+++ b/VocaDbWeb/webpack.mix.js
@@ -25,6 +25,7 @@ mix
processCssUrls: false,
})
.alias({
+ '@Components': path.join(__dirname, 'Scripts/Components'),
'@DataContracts': path.join(__dirname, 'Scripts/DataContracts'),
'@Helpers': path.join(__dirname, 'Scripts/Helpers'),
'@KnockoutExtensions': path.join(__dirname, 'Scripts/KnockoutExtensions'),
@@ -78,7 +79,10 @@ mix
.styles(
['wwwroot/Scripts/jqwidgets27/styles/jqx.base.css'],
'wwwroot/Scripts/jqwidgets27/styles/css.css',
- );
+ )
+
+ .ts('Scripts/index.tsx', 'wwwroot/bundles')
+ .react();
if (mix.inProduction()) {
mix.version();