ChandimaPrabath
commited on
Commit
•
0527ba9
1
Parent(s):
804544a
update
Browse files- frontend/package-lock.json +149 -4
- frontend/package.json +10 -5
- frontend/src/api/apiClient.js +6 -0
- frontend/src/api/searchApi.js +21 -0
- frontend/src/app/favicon.ico +0 -0
- frontend/src/app/films/filmsPage.css +78 -0
- frontend/src/app/films/page.js +87 -0
- frontend/src/app/layout.js +25 -2
- frontend/src/app/page.js +34 -104
- frontend/src/app/player/film/[title]/page.js +16 -0
- frontend/src/app/player/tvshow/[title]/[season]/[episode]/page.js +16 -0
- frontend/src/app/search/page.js +106 -0
- frontend/src/app/search/searchPage.css +189 -0
- frontend/src/app/tvshows/page.js +11 -0
- frontend/src/components/Navbar.css +89 -0
- frontend/src/components/Navbar.js +119 -0
- frontend/src/components/film/card.css +193 -0
- frontend/src/components/film/card.js +92 -0
- frontend/src/context/FilmContext.js +17 -0
- frontend/src/context/TvshowContext.js +18 -0
- frontend/src/lib/LoadBalancer.js +120 -0
- frontend/src/lib/config.js +7 -0
frontend/package-lock.json
CHANGED
@@ -8,6 +8,11 @@
|
|
8 |
"name": "frontend",
|
9 |
"version": "0.1.0",
|
10 |
"dependencies": {
|
|
|
|
|
|
|
|
|
|
|
11 |
"next": "14.2.5",
|
12 |
"react": "^18",
|
13 |
"react-dom": "^18"
|
@@ -87,6 +92,59 @@
|
|
87 |
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
88 |
}
|
89 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
"node_modules/@humanwhocodes/config-array": {
|
91 |
"version": "0.11.14",
|
92 |
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
@@ -833,6 +891,11 @@
|
|
833 |
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
|
834 |
"dev": true
|
835 |
},
|
|
|
|
|
|
|
|
|
|
|
836 |
"node_modules/available-typed-arrays": {
|
837 |
"version": "1.0.7",
|
838 |
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
@@ -857,6 +920,16 @@
|
|
857 |
"node": ">=4"
|
858 |
}
|
859 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
860 |
"node_modules/axobject-query": {
|
861 |
"version": "3.1.1",
|
862 |
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
|
@@ -1048,6 +1121,17 @@
|
|
1048 |
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
1049 |
"dev": true
|
1050 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1051 |
"node_modules/commander": {
|
1052 |
"version": "4.1.1",
|
1053 |
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
@@ -1235,6 +1319,14 @@
|
|
1235 |
"url": "https://github.com/sponsors/ljharb"
|
1236 |
}
|
1237 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1238 |
"node_modules/didyoumean": {
|
1239 |
"version": "1.2.2",
|
1240 |
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
@@ -2013,6 +2105,25 @@
|
|
2013 |
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
2014 |
"dev": true
|
2015 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2016 |
"node_modules/for-each": {
|
2017 |
"version": "0.3.3",
|
2018 |
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
@@ -2038,6 +2149,19 @@
|
|
2038 |
"url": "https://github.com/sponsors/isaacs"
|
2039 |
}
|
2040 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2041 |
"node_modules/fs.realpath": {
|
2042 |
"version": "1.0.0",
|
2043 |
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
@@ -3035,6 +3159,25 @@
|
|
3035 |
"node": ">=8.6"
|
3036 |
}
|
3037 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3038 |
"node_modules/minimatch": {
|
3039 |
"version": "3.1.2",
|
3040 |
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
@@ -3194,7 +3337,6 @@
|
|
3194 |
"version": "4.1.1",
|
3195 |
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
3196 |
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
3197 |
-
"dev": true,
|
3198 |
"engines": {
|
3199 |
"node": ">=0.10.0"
|
3200 |
}
|
@@ -3664,13 +3806,17 @@
|
|
3664 |
"version": "15.8.1",
|
3665 |
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
3666 |
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
3667 |
-
"dev": true,
|
3668 |
"dependencies": {
|
3669 |
"loose-envify": "^1.4.0",
|
3670 |
"object-assign": "^4.1.1",
|
3671 |
"react-is": "^16.13.1"
|
3672 |
}
|
3673 |
},
|
|
|
|
|
|
|
|
|
|
|
3674 |
"node_modules/punycode": {
|
3675 |
"version": "2.3.1",
|
3676 |
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
@@ -3726,8 +3872,7 @@
|
|
3726 |
"node_modules/react-is": {
|
3727 |
"version": "16.13.1",
|
3728 |
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
3729 |
-
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
3730 |
-
"dev": true
|
3731 |
},
|
3732 |
"node_modules/read-cache": {
|
3733 |
"version": "1.0.0",
|
|
|
8 |
"name": "frontend",
|
9 |
"version": "0.1.0",
|
10 |
"dependencies": {
|
11 |
+
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
12 |
+
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
13 |
+
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
14 |
+
"@fortawesome/react-fontawesome": "^0.2.2",
|
15 |
+
"axios": "^1.7.4",
|
16 |
"next": "14.2.5",
|
17 |
"react": "^18",
|
18 |
"react-dom": "^18"
|
|
|
92 |
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
93 |
}
|
94 |
},
|
95 |
+
"node_modules/@fortawesome/fontawesome-common-types": {
|
96 |
+
"version": "6.6.0",
|
97 |
+
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
|
98 |
+
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
|
99 |
+
"engines": {
|
100 |
+
"node": ">=6"
|
101 |
+
}
|
102 |
+
},
|
103 |
+
"node_modules/@fortawesome/fontawesome-svg-core": {
|
104 |
+
"version": "6.6.0",
|
105 |
+
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
|
106 |
+
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
|
107 |
+
"dependencies": {
|
108 |
+
"@fortawesome/fontawesome-common-types": "6.6.0"
|
109 |
+
},
|
110 |
+
"engines": {
|
111 |
+
"node": ">=6"
|
112 |
+
}
|
113 |
+
},
|
114 |
+
"node_modules/@fortawesome/free-regular-svg-icons": {
|
115 |
+
"version": "6.6.0",
|
116 |
+
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz",
|
117 |
+
"integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==",
|
118 |
+
"dependencies": {
|
119 |
+
"@fortawesome/fontawesome-common-types": "6.6.0"
|
120 |
+
},
|
121 |
+
"engines": {
|
122 |
+
"node": ">=6"
|
123 |
+
}
|
124 |
+
},
|
125 |
+
"node_modules/@fortawesome/free-solid-svg-icons": {
|
126 |
+
"version": "6.6.0",
|
127 |
+
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
|
128 |
+
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
|
129 |
+
"dependencies": {
|
130 |
+
"@fortawesome/fontawesome-common-types": "6.6.0"
|
131 |
+
},
|
132 |
+
"engines": {
|
133 |
+
"node": ">=6"
|
134 |
+
}
|
135 |
+
},
|
136 |
+
"node_modules/@fortawesome/react-fontawesome": {
|
137 |
+
"version": "0.2.2",
|
138 |
+
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
|
139 |
+
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
|
140 |
+
"dependencies": {
|
141 |
+
"prop-types": "^15.8.1"
|
142 |
+
},
|
143 |
+
"peerDependencies": {
|
144 |
+
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
145 |
+
"react": ">=16.3"
|
146 |
+
}
|
147 |
+
},
|
148 |
"node_modules/@humanwhocodes/config-array": {
|
149 |
"version": "0.11.14",
|
150 |
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
|
|
891 |
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
|
892 |
"dev": true
|
893 |
},
|
894 |
+
"node_modules/asynckit": {
|
895 |
+
"version": "0.4.0",
|
896 |
+
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
897 |
+
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
898 |
+
},
|
899 |
"node_modules/available-typed-arrays": {
|
900 |
"version": "1.0.7",
|
901 |
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
|
|
920 |
"node": ">=4"
|
921 |
}
|
922 |
},
|
923 |
+
"node_modules/axios": {
|
924 |
+
"version": "1.7.4",
|
925 |
+
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
926 |
+
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
927 |
+
"dependencies": {
|
928 |
+
"follow-redirects": "^1.15.6",
|
929 |
+
"form-data": "^4.0.0",
|
930 |
+
"proxy-from-env": "^1.1.0"
|
931 |
+
}
|
932 |
+
},
|
933 |
"node_modules/axobject-query": {
|
934 |
"version": "3.1.1",
|
935 |
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
|
|
|
1121 |
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
1122 |
"dev": true
|
1123 |
},
|
1124 |
+
"node_modules/combined-stream": {
|
1125 |
+
"version": "1.0.8",
|
1126 |
+
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
1127 |
+
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
1128 |
+
"dependencies": {
|
1129 |
+
"delayed-stream": "~1.0.0"
|
1130 |
+
},
|
1131 |
+
"engines": {
|
1132 |
+
"node": ">= 0.8"
|
1133 |
+
}
|
1134 |
+
},
|
1135 |
"node_modules/commander": {
|
1136 |
"version": "4.1.1",
|
1137 |
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
|
|
1319 |
"url": "https://github.com/sponsors/ljharb"
|
1320 |
}
|
1321 |
},
|
1322 |
+
"node_modules/delayed-stream": {
|
1323 |
+
"version": "1.0.0",
|
1324 |
+
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
1325 |
+
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
1326 |
+
"engines": {
|
1327 |
+
"node": ">=0.4.0"
|
1328 |
+
}
|
1329 |
+
},
|
1330 |
"node_modules/didyoumean": {
|
1331 |
"version": "1.2.2",
|
1332 |
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
|
|
2105 |
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
2106 |
"dev": true
|
2107 |
},
|
2108 |
+
"node_modules/follow-redirects": {
|
2109 |
+
"version": "1.15.6",
|
2110 |
+
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
2111 |
+
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
2112 |
+
"funding": [
|
2113 |
+
{
|
2114 |
+
"type": "individual",
|
2115 |
+
"url": "https://github.com/sponsors/RubenVerborgh"
|
2116 |
+
}
|
2117 |
+
],
|
2118 |
+
"engines": {
|
2119 |
+
"node": ">=4.0"
|
2120 |
+
},
|
2121 |
+
"peerDependenciesMeta": {
|
2122 |
+
"debug": {
|
2123 |
+
"optional": true
|
2124 |
+
}
|
2125 |
+
}
|
2126 |
+
},
|
2127 |
"node_modules/for-each": {
|
2128 |
"version": "0.3.3",
|
2129 |
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
|
|
2149 |
"url": "https://github.com/sponsors/isaacs"
|
2150 |
}
|
2151 |
},
|
2152 |
+
"node_modules/form-data": {
|
2153 |
+
"version": "4.0.0",
|
2154 |
+
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
2155 |
+
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
2156 |
+
"dependencies": {
|
2157 |
+
"asynckit": "^0.4.0",
|
2158 |
+
"combined-stream": "^1.0.8",
|
2159 |
+
"mime-types": "^2.1.12"
|
2160 |
+
},
|
2161 |
+
"engines": {
|
2162 |
+
"node": ">= 6"
|
2163 |
+
}
|
2164 |
+
},
|
2165 |
"node_modules/fs.realpath": {
|
2166 |
"version": "1.0.0",
|
2167 |
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
|
|
3159 |
"node": ">=8.6"
|
3160 |
}
|
3161 |
},
|
3162 |
+
"node_modules/mime-db": {
|
3163 |
+
"version": "1.52.0",
|
3164 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
3165 |
+
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
3166 |
+
"engines": {
|
3167 |
+
"node": ">= 0.6"
|
3168 |
+
}
|
3169 |
+
},
|
3170 |
+
"node_modules/mime-types": {
|
3171 |
+
"version": "2.1.35",
|
3172 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
3173 |
+
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
3174 |
+
"dependencies": {
|
3175 |
+
"mime-db": "1.52.0"
|
3176 |
+
},
|
3177 |
+
"engines": {
|
3178 |
+
"node": ">= 0.6"
|
3179 |
+
}
|
3180 |
+
},
|
3181 |
"node_modules/minimatch": {
|
3182 |
"version": "3.1.2",
|
3183 |
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
|
|
3337 |
"version": "4.1.1",
|
3338 |
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
3339 |
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
|
|
3340 |
"engines": {
|
3341 |
"node": ">=0.10.0"
|
3342 |
}
|
|
|
3806 |
"version": "15.8.1",
|
3807 |
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
3808 |
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
|
|
3809 |
"dependencies": {
|
3810 |
"loose-envify": "^1.4.0",
|
3811 |
"object-assign": "^4.1.1",
|
3812 |
"react-is": "^16.13.1"
|
3813 |
}
|
3814 |
},
|
3815 |
+
"node_modules/proxy-from-env": {
|
3816 |
+
"version": "1.1.0",
|
3817 |
+
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
3818 |
+
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
3819 |
+
},
|
3820 |
"node_modules/punycode": {
|
3821 |
"version": "2.3.1",
|
3822 |
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
|
|
3872 |
"node_modules/react-is": {
|
3873 |
"version": "16.13.1",
|
3874 |
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
3875 |
+
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
|
|
3876 |
},
|
3877 |
"node_modules/read-cache": {
|
3878 |
"version": "1.0.0",
|
frontend/package.json
CHANGED
@@ -9,14 +9,19 @@
|
|
9 |
"lint": "next lint"
|
10 |
},
|
11 |
"dependencies": {
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
"react": "^18",
|
13 |
-
"react-dom": "^18"
|
14 |
-
"next": "14.2.5"
|
15 |
},
|
16 |
"devDependencies": {
|
17 |
-
"postcss": "^8",
|
18 |
-
"tailwindcss": "^3.4.1",
|
19 |
"eslint": "^8",
|
20 |
-
"eslint-config-next": "14.2.5"
|
|
|
|
|
21 |
}
|
22 |
}
|
|
|
9 |
"lint": "next lint"
|
10 |
},
|
11 |
"dependencies": {
|
12 |
+
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
13 |
+
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
14 |
+
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
15 |
+
"@fortawesome/react-fontawesome": "^0.2.2",
|
16 |
+
"axios": "^1.7.4",
|
17 |
+
"next": "14.2.5",
|
18 |
"react": "^18",
|
19 |
+
"react-dom": "^18"
|
|
|
20 |
},
|
21 |
"devDependencies": {
|
|
|
|
|
22 |
"eslint": "^8",
|
23 |
+
"eslint-config-next": "14.2.5",
|
24 |
+
"postcss": "^8",
|
25 |
+
"tailwindcss": "^3.4.1"
|
26 |
}
|
27 |
}
|
frontend/src/api/apiClient.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { LoadBalancerAPI } from '@/lib/LoadBalancer';
|
2 |
+
import config from '@/lib/config';
|
3 |
+
|
4 |
+
const apiClient = new LoadBalancerAPI(config.apiBaseUrl);
|
5 |
+
|
6 |
+
export default apiClient;
|
frontend/src/api/searchApi.js
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// lib/searchApi.js or utils/searchApi.js based on your structure
|
2 |
+
import axios from 'axios';
|
3 |
+
import config from '@/lib/config'; // Adjust path if necessary
|
4 |
+
|
5 |
+
const searchUrl = config.searchUrl;
|
6 |
+
|
7 |
+
/**
|
8 |
+
* Perform a search query.
|
9 |
+
* @param {string} query - The search query.
|
10 |
+
* @returns {Promise<Object>} - The search results.
|
11 |
+
*/
|
12 |
+
export const search = async (query) => {
|
13 |
+
try {
|
14 |
+
const response = await axios.post(searchUrl, { query });
|
15 |
+
console.log(response);
|
16 |
+
return response.data;
|
17 |
+
} catch (error) {
|
18 |
+
console.error("Error performing search:", error);
|
19 |
+
throw error; // Rethrow the error to be handled by the caller
|
20 |
+
}
|
21 |
+
};
|
frontend/src/app/favicon.ico
DELETED
Binary file (25.9 kB)
|
|
frontend/src/app/films/filmsPage.css
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.films-page-container {
|
2 |
+
display: flex;
|
3 |
+
flex-direction: column;
|
4 |
+
align-items: center;
|
5 |
+
padding: 20px;
|
6 |
+
max-width: 1300px;
|
7 |
+
margin: 0 auto;
|
8 |
+
}
|
9 |
+
|
10 |
+
.films-page {
|
11 |
+
display: grid;
|
12 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
13 |
+
gap: 50px;
|
14 |
+
justify-items: center;
|
15 |
+
align-items: start;
|
16 |
+
width: 100%;
|
17 |
+
}
|
18 |
+
|
19 |
+
/* Media query for smaller screens */
|
20 |
+
@media (max-width: 768px) {
|
21 |
+
.films-page {
|
22 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, .1fr));
|
23 |
+
gap: 10px;
|
24 |
+
}
|
25 |
+
}
|
26 |
+
|
27 |
+
@media (max-width: 480px) {
|
28 |
+
.films-page {
|
29 |
+
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
30 |
+
}
|
31 |
+
}
|
32 |
+
|
33 |
+
.pagination-controls {
|
34 |
+
margin-top: 20px;
|
35 |
+
display: flex;
|
36 |
+
align-items: center;
|
37 |
+
}
|
38 |
+
|
39 |
+
.pagination-button {
|
40 |
+
background-color: #2c2b2b;
|
41 |
+
color: #f5f5f5;
|
42 |
+
border: none;
|
43 |
+
border-radius: 5px;
|
44 |
+
padding: 5px;
|
45 |
+
width: 50px;
|
46 |
+
text-align: center;
|
47 |
+
margin: 0 10px;
|
48 |
+
cursor: pointer;
|
49 |
+
transition: background-color 0.3s ease, transform 0.3s ease;
|
50 |
+
}
|
51 |
+
|
52 |
+
.pagination-button:hover {
|
53 |
+
background-color: #555;
|
54 |
+
transform: scale(1.05);
|
55 |
+
}
|
56 |
+
|
57 |
+
.pagination-button:disabled {
|
58 |
+
background-color: #555;
|
59 |
+
cursor: not-allowed;
|
60 |
+
}
|
61 |
+
|
62 |
+
.page-info {
|
63 |
+
font-size: 1.2em;
|
64 |
+
color: #f5f5f5;
|
65 |
+
}
|
66 |
+
|
67 |
+
/* Handle animations on page load */
|
68 |
+
@keyframes pageLoad {
|
69 |
+
from {
|
70 |
+
opacity: 0;
|
71 |
+
}
|
72 |
+
to {
|
73 |
+
opacity: 1;
|
74 |
+
}
|
75 |
+
}
|
76 |
+
.films-page-container {
|
77 |
+
animation: pageLoad 1s ease;
|
78 |
+
}
|
frontend/src/app/films/page.js
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
import { useEffect, useState } from 'react';
|
3 |
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
4 |
+
import apiClient from '@/api/apiClient'; // Updated for Next.js absolute import
|
5 |
+
import FilmCard from '@/components/film/card'; // Updated for Next.js absolute import
|
6 |
+
import { useFilmContext } from '@/context/FilmContext'; // Updated for Next.js absolute import
|
7 |
+
import './filmsPage.css'; // Updated for Next.js absolute import
|
8 |
+
|
9 |
+
import { faCaretLeft } from '@fortawesome/free-solid-svg-icons';
|
10 |
+
import { faCaretRight } from '@fortawesome/free-solid-svg-icons';
|
11 |
+
|
12 |
+
const FILMS_PER_PAGE = 2;
|
13 |
+
|
14 |
+
export default function FilmsPage() {
|
15 |
+
const { films, setFilms } = useFilmContext();
|
16 |
+
const [currentPage, setCurrentPage] = useState(1);
|
17 |
+
|
18 |
+
useEffect(() => {
|
19 |
+
if (films.length === 0) {
|
20 |
+
apiClient.getAllFilms()
|
21 |
+
.then(response => {
|
22 |
+
console.log('All films:', response);
|
23 |
+
setFilms(response.map(film => film.replace('films/', '')));
|
24 |
+
})
|
25 |
+
.catch(error => {
|
26 |
+
console.error('Failed to get all films:', error);
|
27 |
+
});
|
28 |
+
}
|
29 |
+
}, [films, setFilms]);
|
30 |
+
|
31 |
+
const startIndex = (currentPage - 1) * FILMS_PER_PAGE;
|
32 |
+
const currentFilms = films.slice(startIndex, startIndex + FILMS_PER_PAGE);
|
33 |
+
|
34 |
+
const handleNextPage = () => {
|
35 |
+
setCurrentPage(prevPage => prevPage + 1);
|
36 |
+
};
|
37 |
+
|
38 |
+
const handlePrevPage = () => {
|
39 |
+
setCurrentPage(prevPage => Math.max(prevPage - 1, 1));
|
40 |
+
};
|
41 |
+
|
42 |
+
// Calculate total number of pages
|
43 |
+
const totalPages = Math.ceil(films.length / FILMS_PER_PAGE);
|
44 |
+
|
45 |
+
// Determine if the previous and next buttons should be enabled
|
46 |
+
const isPrevButtonEnabled = currentPage > 1;
|
47 |
+
const isNextButtonEnabled = currentPage < totalPages;
|
48 |
+
|
49 |
+
return (
|
50 |
+
<div className="films-page-container">
|
51 |
+
<div className="pagination-controls">
|
52 |
+
<button
|
53 |
+
onClick={handlePrevPage}
|
54 |
+
disabled={!isPrevButtonEnabled}
|
55 |
+
className="pagination-button"
|
56 |
+
>
|
57 |
+
<FontAwesomeIcon
|
58 |
+
icon={faCaretLeft}
|
59 |
+
size="2xl"
|
60 |
+
color='#e88f36'
|
61 |
+
bounce={isPrevButtonEnabled}
|
62 |
+
/>
|
63 |
+
</button>
|
64 |
+
<span className="page-info">
|
65 |
+
{currentPage} - {totalPages}
|
66 |
+
</span>
|
67 |
+
<button
|
68 |
+
onClick={handleNextPage}
|
69 |
+
disabled={!isNextButtonEnabled}
|
70 |
+
className="pagination-button"
|
71 |
+
>
|
72 |
+
<FontAwesomeIcon
|
73 |
+
icon={faCaretRight}
|
74 |
+
size="2xl"
|
75 |
+
color='#e88f36'
|
76 |
+
bounce={isNextButtonEnabled}
|
77 |
+
/>
|
78 |
+
</button>
|
79 |
+
</div>
|
80 |
+
<div className="films-page">
|
81 |
+
{currentFilms.map(title => (
|
82 |
+
<FilmCard key={title} title={title} />
|
83 |
+
))}
|
84 |
+
</div>
|
85 |
+
</div>
|
86 |
+
);
|
87 |
+
}
|
frontend/src/app/layout.js
CHANGED
@@ -1,17 +1,40 @@
|
|
1 |
import { Inter } from "next/font/google";
|
2 |
import "./globals.css";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
|
4 |
const inter = Inter({ subsets: ["latin"] });
|
5 |
|
6 |
export const metadata = {
|
7 |
-
title: "
|
8 |
description: "Generated by create next app",
|
9 |
};
|
10 |
|
11 |
export default function RootLayout({ children }) {
|
12 |
return (
|
13 |
<html lang="en">
|
14 |
-
<body className={inter.className}>
|
|
|
|
|
|
|
|
|
|
|
15 |
</html>
|
16 |
);
|
17 |
}
|
|
|
1 |
import { Inter } from "next/font/google";
|
2 |
import "./globals.css";
|
3 |
+
import Navbar from "../components/Navbar";
|
4 |
+
import { FilmProvider } from "@/context/FilmContext";
|
5 |
+
import { library } from "@fortawesome/fontawesome-svg-core";
|
6 |
+
import { config } from "@fortawesome/fontawesome-svg-core";
|
7 |
+
import "@fortawesome/fontawesome-svg-core/styles.css";
|
8 |
+
config.autoAddCss = false;
|
9 |
+
import {
|
10 |
+
faEllipsisVertical,
|
11 |
+
faPlay,
|
12 |
+
faChevronLeft,
|
13 |
+
faCaretDown,
|
14 |
+
faCaretLeft,
|
15 |
+
faCaretRight,
|
16 |
+
faMagnifyingGlass,
|
17 |
+
faHome,
|
18 |
+
faFilm,
|
19 |
+
faTv,
|
20 |
+
} from "@fortawesome/free-solid-svg-icons";
|
21 |
|
22 |
const inter = Inter({ subsets: ["latin"] });
|
23 |
|
24 |
export const metadata = {
|
25 |
+
title: "Home",
|
26 |
description: "Generated by create next app",
|
27 |
};
|
28 |
|
29 |
export default function RootLayout({ children }) {
|
30 |
return (
|
31 |
<html lang="en">
|
32 |
+
<body className={inter.className}>
|
33 |
+
<header>
|
34 |
+
<Navbar />
|
35 |
+
</header>
|
36 |
+
<FilmProvider>{children}</FilmProvider>
|
37 |
+
</body>
|
38 |
</html>
|
39 |
);
|
40 |
}
|
frontend/src/app/page.js
CHANGED
@@ -1,113 +1,43 @@
|
|
1 |
-
|
|
|
|
|
2 |
|
3 |
export default function Home() {
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
8 |
-
Get started by editing
|
9 |
-
<code className="font-mono font-bold">src/app/page.js</code>
|
10 |
-
</p>
|
11 |
-
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
|
12 |
-
<a
|
13 |
-
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
14 |
-
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
15 |
-
target="_blank"
|
16 |
-
rel="noopener noreferrer"
|
17 |
-
>
|
18 |
-
By{" "}
|
19 |
-
<Image
|
20 |
-
src="/vercel.svg"
|
21 |
-
alt="Vercel Logo"
|
22 |
-
className="dark:invert"
|
23 |
-
width={100}
|
24 |
-
height={24}
|
25 |
-
priority
|
26 |
-
/>
|
27 |
-
</a>
|
28 |
-
</div>
|
29 |
-
</div>
|
30 |
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
|
|
|
|
|
|
41 |
|
42 |
-
|
43 |
-
|
44 |
-
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
45 |
-
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
46 |
-
target="_blank"
|
47 |
-
rel="noopener noreferrer"
|
48 |
-
>
|
49 |
-
<h2 className={`mb-3 text-2xl font-semibold`}>
|
50 |
-
Docs{" "}
|
51 |
-
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
52 |
-
->
|
53 |
-
</span>
|
54 |
-
</h2>
|
55 |
-
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
56 |
-
Find in-depth information about Next.js features and API.
|
57 |
-
</p>
|
58 |
-
</a>
|
59 |
|
60 |
-
|
61 |
-
|
62 |
-
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800 hover:dark:bg-opacity-30"
|
63 |
-
target="_blank"
|
64 |
-
rel="noopener noreferrer"
|
65 |
-
>
|
66 |
-
<h2 className={`mb-3 text-2xl font-semibold`}>
|
67 |
-
Learn{" "}
|
68 |
-
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
69 |
-
->
|
70 |
-
</span>
|
71 |
-
</h2>
|
72 |
-
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
73 |
-
Learn about Next.js in an interactive course with quizzes!
|
74 |
-
</p>
|
75 |
-
</a>
|
76 |
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
>
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
</span>
|
88 |
-
</h2>
|
89 |
-
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
90 |
-
Explore starter templates for Next.js.
|
91 |
-
</p>
|
92 |
-
</a>
|
93 |
-
|
94 |
-
<a
|
95 |
-
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
96 |
-
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
97 |
-
target="_blank"
|
98 |
-
rel="noopener noreferrer"
|
99 |
-
>
|
100 |
-
<h2 className={`mb-3 text-2xl font-semibold`}>
|
101 |
-
Deploy{" "}
|
102 |
-
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
103 |
-
->
|
104 |
-
</span>
|
105 |
-
</h2>
|
106 |
-
<p className={`m-0 max-w-[30ch] text-sm opacity-50 text-balance`}>
|
107 |
-
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
108 |
-
</p>
|
109 |
-
</a>
|
110 |
</div>
|
111 |
-
|
112 |
);
|
113 |
}
|
|
|
1 |
+
'use client';
|
2 |
+
import { useEffect, useState } from 'react';
|
3 |
+
import apiClient from "@/api/apiClient";
|
4 |
|
5 |
export default function Home() {
|
6 |
+
const [films, setFilms] = useState([]);
|
7 |
+
const [loading, setLoading] = useState(true);
|
8 |
+
const [error, setError] = useState(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
+
useEffect(() => {
|
11 |
+
async function fetchFilms() {
|
12 |
+
try {
|
13 |
+
const all_films = await apiClient.getAllFilms();
|
14 |
+
setFilms(all_films);
|
15 |
+
console.log(all_films);
|
16 |
+
} catch (error) {
|
17 |
+
setError('Failed to load films.');
|
18 |
+
console.error('Error fetching films:', error);
|
19 |
+
} finally {
|
20 |
+
setLoading(false);
|
21 |
+
}
|
22 |
+
}
|
23 |
|
24 |
+
fetchFilms();
|
25 |
+
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
|
27 |
+
if (loading) return <p>Loading...</p>;
|
28 |
+
if (error) return <p>{error}</p>;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
|
30 |
+
return (
|
31 |
+
<>
|
32 |
+
<div>
|
33 |
+
<h1>Welcome to the Streaming Service</h1>
|
34 |
+
<p>Explore our collection of films and TV shows.</p>
|
35 |
+
<ul>
|
36 |
+
{films.map((film, index) => (
|
37 |
+
<li key={index}>{film.title}</li>
|
38 |
+
))}
|
39 |
+
</ul>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
</div>
|
41 |
+
</>
|
42 |
);
|
43 |
}
|
frontend/src/app/player/film/[title]/page.js
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { useParams } from 'next/navigation';
|
4 |
+
|
5 |
+
export default function FilmPlayer() {
|
6 |
+
const { title } = useParams();
|
7 |
+
|
8 |
+
return (
|
9 |
+
<>
|
10 |
+
<div>
|
11 |
+
<h1 className='text-white'>Film Player - {title}</h1>
|
12 |
+
{/* Add film player component here */}
|
13 |
+
</div>
|
14 |
+
</>
|
15 |
+
);
|
16 |
+
}
|
frontend/src/app/player/tvshow/[title]/[season]/[episode]/page.js
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import { useParams } from "next/navigation";
|
4 |
+
|
5 |
+
export default function TVShowPlayer() {
|
6 |
+
const { title, season, episode } = useParams();
|
7 |
+
|
8 |
+
return (
|
9 |
+
<>
|
10 |
+
<div>
|
11 |
+
<h1 className="text-white">TV Show Player - {title + season + episode}</h1>
|
12 |
+
{/* Add TV show player component here */}
|
13 |
+
</div>
|
14 |
+
</>
|
15 |
+
);
|
16 |
+
}
|
frontend/src/app/search/page.js
ADDED
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"; // Indicate that this file is a client component in Next.js
|
2 |
+
|
3 |
+
import { useState, useEffect } from "react";
|
4 |
+
import { useRouter } from "next/navigation";
|
5 |
+
import { search } from "@/api/searchApi"; // Ensure the path is correct for your API
|
6 |
+
import "./searchPage.css";
|
7 |
+
|
8 |
+
const SearchPage = () => {
|
9 |
+
const [query, setQuery] = useState("");
|
10 |
+
const [results, setResults] = useState({ films: [], tv_series: [] });
|
11 |
+
const [loading, setLoading] = useState(false);
|
12 |
+
const [error, setError] = useState(null);
|
13 |
+
const [debouncedQuery, setDebouncedQuery] = useState("");
|
14 |
+
|
15 |
+
const router = useRouter(); // Use Next.js useRouter for navigation
|
16 |
+
|
17 |
+
useEffect(() => {
|
18 |
+
const handler = setTimeout(() => {
|
19 |
+
if (debouncedQuery) {
|
20 |
+
handleSearch();
|
21 |
+
}
|
22 |
+
}, 200); // Delay of 200ms
|
23 |
+
|
24 |
+
return () => {
|
25 |
+
clearTimeout(handler);
|
26 |
+
};
|
27 |
+
}, [debouncedQuery]);
|
28 |
+
|
29 |
+
const handleSearch = async () => {
|
30 |
+
setLoading(true);
|
31 |
+
setError(null);
|
32 |
+
try {
|
33 |
+
const data = await search(debouncedQuery);
|
34 |
+
setResults(data);
|
35 |
+
} catch (err) {
|
36 |
+
setError("Failed to fetch search results.");
|
37 |
+
} finally {
|
38 |
+
setLoading(false);
|
39 |
+
}
|
40 |
+
};
|
41 |
+
|
42 |
+
const handleChange = (e) => {
|
43 |
+
setQuery(e.target.value);
|
44 |
+
setDebouncedQuery(e.target.value);
|
45 |
+
};
|
46 |
+
|
47 |
+
const handleItemClick = (path) => {
|
48 |
+
router.push(path); // Use router.push for navigation
|
49 |
+
};
|
50 |
+
|
51 |
+
return (
|
52 |
+
<div className="search-page">
|
53 |
+
<div className="search-container">
|
54 |
+
<input
|
55 |
+
type="text"
|
56 |
+
value={query}
|
57 |
+
onChange={handleChange}
|
58 |
+
placeholder="Search for films or TV shows..."
|
59 |
+
className="search-input"
|
60 |
+
/>
|
61 |
+
{loading && (
|
62 |
+
<div className="loading-indicator">
|
63 |
+
<div className="spinner"></div>
|
64 |
+
</div>
|
65 |
+
)}
|
66 |
+
</div>
|
67 |
+
{error && <div className="error-message">{error}</div>}
|
68 |
+
<div className="results-container">
|
69 |
+
{results.films.length > 0 && (
|
70 |
+
<div className="results-section">
|
71 |
+
<h2>Films</h2>
|
72 |
+
<ul className="results-list">
|
73 |
+
{results.films.map((film, index) => (
|
74 |
+
<li
|
75 |
+
key={index}
|
76 |
+
className="result-item"
|
77 |
+
onClick={() => handleItemClick(`/film/${encodeURIComponent(film)}`)}
|
78 |
+
>
|
79 |
+
{film}
|
80 |
+
</li>
|
81 |
+
))}
|
82 |
+
</ul>
|
83 |
+
</div>
|
84 |
+
)}
|
85 |
+
{results.tv_series.length > 0 && (
|
86 |
+
<div className="results-section">
|
87 |
+
<h2>TV Series</h2>
|
88 |
+
<ul className="results-list">
|
89 |
+
{results.tv_series.map((series, index) => (
|
90 |
+
<li
|
91 |
+
key={index}
|
92 |
+
className="result-item"
|
93 |
+
onClick={() => handleItemClick(`/tvshow/${encodeURIComponent(series)}`)}
|
94 |
+
>
|
95 |
+
{series}
|
96 |
+
</li>
|
97 |
+
))}
|
98 |
+
</ul>
|
99 |
+
</div>
|
100 |
+
)}
|
101 |
+
</div>
|
102 |
+
</div>
|
103 |
+
);
|
104 |
+
};
|
105 |
+
|
106 |
+
export default SearchPage;
|
frontend/src/app/search/searchPage.css
ADDED
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.search-page {
|
2 |
+
background-color: #121212;
|
3 |
+
color: #ffffff;
|
4 |
+
min-height: 85vh;
|
5 |
+
transition: background-color 0.3s ease;
|
6 |
+
font-family: "Signika", sans-serif;
|
7 |
+
font-optical-sizing: auto;
|
8 |
+
font-style: normal;
|
9 |
+
font-variation-settings: "GRAD" 0;
|
10 |
+
}
|
11 |
+
|
12 |
+
/* Search container styles */
|
13 |
+
.search-container {
|
14 |
+
display: flex;
|
15 |
+
flex-direction: column;
|
16 |
+
align-items: center;
|
17 |
+
padding: 20px;
|
18 |
+
transition: transform 0.3s ease;
|
19 |
+
}
|
20 |
+
|
21 |
+
/* Input field styling with animation */
|
22 |
+
.search-input {
|
23 |
+
width: 100%;
|
24 |
+
max-width: 600px;
|
25 |
+
padding: 10px;
|
26 |
+
font-size: 16px;
|
27 |
+
border: 1px solid #444;
|
28 |
+
border-radius: 5px;
|
29 |
+
background-color: #1e1e1e;
|
30 |
+
color: #ffffff;
|
31 |
+
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
32 |
+
}
|
33 |
+
|
34 |
+
.search-input:focus {
|
35 |
+
outline: none;
|
36 |
+
border-color: #ff6f00;
|
37 |
+
box-shadow: 0 0 8px rgba(255, 111, 0, 0.6);
|
38 |
+
transform: scale(1.02);
|
39 |
+
}
|
40 |
+
|
41 |
+
/* Loading indicator animation */
|
42 |
+
.loading-indicator {
|
43 |
+
margin-top: 10px;
|
44 |
+
font-size: 18px;
|
45 |
+
color: #ff6f00;
|
46 |
+
opacity: 0;
|
47 |
+
animation: fadeIn 1s forwards;
|
48 |
+
}
|
49 |
+
|
50 |
+
@keyframes fadeIn {
|
51 |
+
from {
|
52 |
+
opacity: 0;
|
53 |
+
}
|
54 |
+
to {
|
55 |
+
opacity: 1;
|
56 |
+
}
|
57 |
+
}
|
58 |
+
|
59 |
+
/* Error message styling */
|
60 |
+
.error-message {
|
61 |
+
color: #ff6f00;
|
62 |
+
text-align: center;
|
63 |
+
margin-top: 20px;
|
64 |
+
animation: bounce 1s infinite;
|
65 |
+
}
|
66 |
+
|
67 |
+
@keyframes bounce {
|
68 |
+
0%, 20%, 50%, 80%, 100% {
|
69 |
+
transform: translateY(0);
|
70 |
+
}
|
71 |
+
40% {
|
72 |
+
transform: translateY(-10px);
|
73 |
+
}
|
74 |
+
60% {
|
75 |
+
transform: translateY(-5px);
|
76 |
+
}
|
77 |
+
}
|
78 |
+
|
79 |
+
/* Results container styles */
|
80 |
+
.results-container {
|
81 |
+
padding: 20px;
|
82 |
+
display: flex;
|
83 |
+
flex-direction: column;
|
84 |
+
align-items: center;
|
85 |
+
}
|
86 |
+
|
87 |
+
/* Results section styles */
|
88 |
+
.results-section {
|
89 |
+
width: 100%;
|
90 |
+
max-width: 800px;
|
91 |
+
margin-bottom: 20px;
|
92 |
+
transition: opacity 0.3s ease;
|
93 |
+
}
|
94 |
+
|
95 |
+
.results-section h2 {
|
96 |
+
font-size: 24px;
|
97 |
+
border-bottom: 2px solid #444;
|
98 |
+
padding-bottom: 10px;
|
99 |
+
margin-bottom: 15px;
|
100 |
+
color: #ff6f00;
|
101 |
+
animation: fadeIn 1s ease;
|
102 |
+
}
|
103 |
+
|
104 |
+
/* Results list styles */
|
105 |
+
.results-list {
|
106 |
+
list-style-type: none;
|
107 |
+
padding: 0;
|
108 |
+
}
|
109 |
+
|
110 |
+
.result-item {
|
111 |
+
background-color: #1e1e1e;
|
112 |
+
border: 1px solid #444;
|
113 |
+
border-radius: 5px;
|
114 |
+
margin-bottom: 10px;
|
115 |
+
padding: 10px;
|
116 |
+
transition: background-color 0.3s ease, transform 0.3s ease;
|
117 |
+
animation: slideIn 0.5s ease;
|
118 |
+
}
|
119 |
+
|
120 |
+
@keyframes slideIn {
|
121 |
+
from {
|
122 |
+
transform: translateY(20px);
|
123 |
+
opacity: 0;
|
124 |
+
}
|
125 |
+
to {
|
126 |
+
transform: translateY(0);
|
127 |
+
opacity: 1;
|
128 |
+
}
|
129 |
+
}
|
130 |
+
|
131 |
+
.result-item:hover {
|
132 |
+
background-color: #333;
|
133 |
+
transform: scale(1.02);
|
134 |
+
}
|
135 |
+
|
136 |
+
/* Link styling */
|
137 |
+
.result-link {
|
138 |
+
text-decoration: none;
|
139 |
+
color: #ffffff;
|
140 |
+
font-size: 18px;
|
141 |
+
transition: color 0.3s ease;
|
142 |
+
}
|
143 |
+
|
144 |
+
.result-link:hover {
|
145 |
+
text-decoration: underline;
|
146 |
+
color: #ff6f00;
|
147 |
+
}
|
148 |
+
|
149 |
+
/* Handle animations on page load */
|
150 |
+
@keyframes pageLoad {
|
151 |
+
from {
|
152 |
+
opacity: 0;
|
153 |
+
}
|
154 |
+
to {
|
155 |
+
opacity: 1;
|
156 |
+
}
|
157 |
+
}
|
158 |
+
.search-page {
|
159 |
+
animation: pageLoad 1s ease;
|
160 |
+
}
|
161 |
+
|
162 |
+
/* Loading indicator styles */
|
163 |
+
.loading-indicator {
|
164 |
+
display: flex;
|
165 |
+
align-items: center;
|
166 |
+
margin-top: 20px;
|
167 |
+
font-size: 18px;
|
168 |
+
color: #ff6f00;
|
169 |
+
}
|
170 |
+
|
171 |
+
.loading-indicator .spinner {
|
172 |
+
width: 20px;
|
173 |
+
height: 20px;
|
174 |
+
border: 4px solid #ff6f00;
|
175 |
+
border-top: 4px solid transparent;
|
176 |
+
border-radius: 50%;
|
177 |
+
margin-right: 10px;
|
178 |
+
animation: spin 1s linear infinite;
|
179 |
+
}
|
180 |
+
|
181 |
+
/* Spinner animation */
|
182 |
+
@keyframes spin {
|
183 |
+
from {
|
184 |
+
transform: rotate(0deg);
|
185 |
+
}
|
186 |
+
to {
|
187 |
+
transform: rotate(360deg);
|
188 |
+
}
|
189 |
+
}
|
frontend/src/app/tvshows/page.js
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export default function TVShows() {
|
2 |
+
return (
|
3 |
+
<>
|
4 |
+
<div>
|
5 |
+
<h1>TV Shows</h1>
|
6 |
+
<p className='text-white'>Browse our TV show collection.</p>
|
7 |
+
{/* Add TV show list here */}
|
8 |
+
</div>
|
9 |
+
</>
|
10 |
+
);
|
11 |
+
}
|
frontend/src/components/Navbar.css
ADDED
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* Container for the navbar and underline */
|
2 |
+
nav {
|
3 |
+
position: relative;
|
4 |
+
}
|
5 |
+
|
6 |
+
.nav-links {
|
7 |
+
list-style: none;
|
8 |
+
padding: 0;
|
9 |
+
margin: 0;
|
10 |
+
display: flex;
|
11 |
+
justify-content: center;
|
12 |
+
position: relative; /* Ensure it's positioned relative for absolute positioning of underlines */
|
13 |
+
}
|
14 |
+
|
15 |
+
@import url('https://fonts.googleapis.com/css2?family=Pacifico&family=Rubik+Burned&family=Rubik+Marker+Hatch&family=Rubik+Maze&family=Rubik+Microbe&family=Rubik:ital,wght@0,300..900;1,300..900&display=swap');
|
16 |
+
|
17 |
+
.nav-link {
|
18 |
+
text-decoration: none;
|
19 |
+
color: #b0b0b0;
|
20 |
+
display: flex;
|
21 |
+
align-items: center;
|
22 |
+
justify-content: center;
|
23 |
+
width: 70px;
|
24 |
+
height: 50px;
|
25 |
+
position: relative;
|
26 |
+
font-size: 1em;
|
27 |
+
font-family: "Signika", sans-serif;
|
28 |
+
font-optical-sizing: auto;
|
29 |
+
font-style: normal;
|
30 |
+
font-variation-settings: "GRAD" 0;
|
31 |
+
font-weight: 600;
|
32 |
+
padding: 0 15px;
|
33 |
+
transition: color 0.3s ease, transform 0.3s ease;
|
34 |
+
box-sizing: border-box;
|
35 |
+
}
|
36 |
+
|
37 |
+
.nav-link-menue {
|
38 |
+
text-decoration: none;
|
39 |
+
color: #b0b0b0;
|
40 |
+
display: flex;
|
41 |
+
align-items: center;
|
42 |
+
justify-content: center;
|
43 |
+
position: relative;
|
44 |
+
padding: 11px 10px;
|
45 |
+
transition: color 0.3s ease, transform 0.3s ease;
|
46 |
+
box-sizing: border-box;
|
47 |
+
}
|
48 |
+
|
49 |
+
.nav-link-menue.active {
|
50 |
+
color: #ffffff;
|
51 |
+
}
|
52 |
+
|
53 |
+
.nav-link-menue:hover {
|
54 |
+
color: #ffffff;
|
55 |
+
transform: translateY(-2px);
|
56 |
+
}
|
57 |
+
|
58 |
+
.nav-link.active {
|
59 |
+
color: #ffffff;
|
60 |
+
}
|
61 |
+
|
62 |
+
.nav-link:hover {
|
63 |
+
color: #ffffff;
|
64 |
+
transform: translateY(-2px);
|
65 |
+
}
|
66 |
+
|
67 |
+
.underline {
|
68 |
+
position: absolute;
|
69 |
+
bottom: -3px; /* Adjust this to position the underline correctly at the bottom of the navbar */
|
70 |
+
height: 3px;
|
71 |
+
transition: width 0.3s ease, left 0.3s ease;
|
72 |
+
}
|
73 |
+
|
74 |
+
.active-underline {
|
75 |
+
background-color: #e88f36; /* Warm orange underline for active route */
|
76 |
+
}
|
77 |
+
|
78 |
+
.hover-underline {
|
79 |
+
background-color: #007bc7; /* Bright blue underline for hover effect */
|
80 |
+
}
|
81 |
+
|
82 |
+
/* Media Queries for Mobile Devices */
|
83 |
+
@media (max-width: 768px) {
|
84 |
+
.nav-link {
|
85 |
+
width: 70px;
|
86 |
+
padding: 10px;
|
87 |
+
}
|
88 |
+
}
|
89 |
+
|
frontend/src/components/Navbar.js
ADDED
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import { useEffect, useState } from "react";
|
3 |
+
import { usePathname } from "next/navigation";
|
4 |
+
import Link from "next/link";
|
5 |
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
6 |
+
import { faHome, faMagnifyingGlass, faFilm, faTv, faCaretDown } from "@fortawesome/free-solid-svg-icons";
|
7 |
+
import "./Navbar.css"; // Ensure CSS is in the correct location
|
8 |
+
|
9 |
+
function Navbar() {
|
10 |
+
const pathname = usePathname(); // Use Next.js usePathname
|
11 |
+
const [hoveredLink, setHoveredLink] = useState(null);
|
12 |
+
const [prevLink, setPrevLink] = useState(null);
|
13 |
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
14 |
+
|
15 |
+
const handleModalClose = () => {
|
16 |
+
setIsModalOpen(false);
|
17 |
+
};
|
18 |
+
|
19 |
+
const handleModalOpen = () => {
|
20 |
+
setIsModalOpen(!isModalOpen);
|
21 |
+
};
|
22 |
+
|
23 |
+
useEffect(() => {
|
24 |
+
const activeLink = document.querySelector(
|
25 |
+
`.nav-link[href="${pathname}"]`
|
26 |
+
);
|
27 |
+
if (activeLink) {
|
28 |
+
setPrevLink(activeLink);
|
29 |
+
}
|
30 |
+
}, [pathname]);
|
31 |
+
|
32 |
+
const handleMouseEnter = (e) => {
|
33 |
+
setPrevLink(document.querySelector(".nav-link.active"));
|
34 |
+
setHoveredLink(e.target);
|
35 |
+
};
|
36 |
+
|
37 |
+
const handleMouseLeave = () => {
|
38 |
+
setHoveredLink(null);
|
39 |
+
};
|
40 |
+
|
41 |
+
return (
|
42 |
+
<nav>
|
43 |
+
<ul className="nav-links">
|
44 |
+
<li>
|
45 |
+
<Link
|
46 |
+
href="/"
|
47 |
+
className={`nav-link ${pathname === "/" ? "active" : ""}`}
|
48 |
+
onMouseEnter={handleMouseEnter}
|
49 |
+
onMouseLeave={handleMouseLeave}
|
50 |
+
>
|
51 |
+
<FontAwesomeIcon icon={faHome} size="lg" />
|
52 |
+
</Link>
|
53 |
+
</li>
|
54 |
+
<li>
|
55 |
+
<Link
|
56 |
+
href="/search"
|
57 |
+
className={`nav-link ${pathname === "/search" ? "active" : ""}`}
|
58 |
+
onMouseEnter={handleMouseEnter}
|
59 |
+
onMouseLeave={handleMouseLeave}
|
60 |
+
>
|
61 |
+
<FontAwesomeIcon icon={faMagnifyingGlass} size="lg" />
|
62 |
+
</Link>
|
63 |
+
</li>
|
64 |
+
<li>
|
65 |
+
<Link
|
66 |
+
href="/films"
|
67 |
+
className={`nav-link ${pathname === "/films" ? "active" : ""}`}
|
68 |
+
onMouseEnter={handleMouseEnter}
|
69 |
+
onMouseLeave={handleMouseLeave}
|
70 |
+
>
|
71 |
+
<FontAwesomeIcon icon={faFilm} size="lg" />
|
72 |
+
</Link>
|
73 |
+
</li>
|
74 |
+
<li>
|
75 |
+
<Link
|
76 |
+
href="/tvshows"
|
77 |
+
className={`nav-link ${pathname === "/tvshows" ? "active" : ""}`}
|
78 |
+
onMouseEnter={handleMouseEnter}
|
79 |
+
onMouseLeave={handleMouseLeave}
|
80 |
+
>
|
81 |
+
<FontAwesomeIcon icon={faTv} size="lg" />
|
82 |
+
</Link>
|
83 |
+
</li>
|
84 |
+
<li>
|
85 |
+
<FontAwesomeIcon
|
86 |
+
className="nav-link-menue"
|
87 |
+
icon={faCaretDown}
|
88 |
+
size="xl"
|
89 |
+
onClick={handleModalOpen}
|
90 |
+
/>
|
91 |
+
</li>
|
92 |
+
</ul>
|
93 |
+
<span
|
94 |
+
className="underline active-underline"
|
95 |
+
style={
|
96 |
+
prevLink
|
97 |
+
? {
|
98 |
+
width: prevLink.offsetWidth,
|
99 |
+
left: prevLink.offsetLeft,
|
100 |
+
}
|
101 |
+
: {}
|
102 |
+
}
|
103 |
+
/>
|
104 |
+
<span
|
105 |
+
className="underline hover-underline"
|
106 |
+
style={
|
107 |
+
hoveredLink
|
108 |
+
? {
|
109 |
+
width: hoveredLink.offsetWidth,
|
110 |
+
left: hoveredLink.offsetLeft,
|
111 |
+
}
|
112 |
+
: {}
|
113 |
+
}
|
114 |
+
/>
|
115 |
+
</nav>
|
116 |
+
);
|
117 |
+
}
|
118 |
+
|
119 |
+
export default Navbar;
|
frontend/src/components/film/card.css
ADDED
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.film-card {
|
2 |
+
box-sizing: border-box;
|
3 |
+
width: 220px;
|
4 |
+
height: 390px;
|
5 |
+
border: 1px solid #333;
|
6 |
+
background-color: #2c2c2c;
|
7 |
+
border-radius: 15px;
|
8 |
+
overflow: hidden;
|
9 |
+
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5);
|
10 |
+
transition: background-color 0.3s ease;
|
11 |
+
display: flex;
|
12 |
+
flex-direction: column;
|
13 |
+
justify-content: space-between;
|
14 |
+
margin: 10px;
|
15 |
+
position: relative;
|
16 |
+
}
|
17 |
+
|
18 |
+
.film-card:hover {
|
19 |
+
background-color: #1e1e1e;
|
20 |
+
}
|
21 |
+
|
22 |
+
.film-card-image {
|
23 |
+
width: 100%;
|
24 |
+
object-fit: cover;
|
25 |
+
border-bottom: 3px solid #f8b525;
|
26 |
+
transition: transform 0.5s ease;
|
27 |
+
position: relative;
|
28 |
+
z-index: 1;
|
29 |
+
}
|
30 |
+
|
31 |
+
.film-card:hover .film-card-image {
|
32 |
+
transform: scale(1.1);
|
33 |
+
}
|
34 |
+
|
35 |
+
.film-card-overlay {
|
36 |
+
position: absolute;
|
37 |
+
top: 0;
|
38 |
+
left: 0;
|
39 |
+
width: 100%;
|
40 |
+
height: 100%;
|
41 |
+
background: linear-gradient(to bottom, rgba(18, 36, 65, 0) 0%, rgba(2, 12, 30, 0.658) 80%);
|
42 |
+
opacity: .6;
|
43 |
+
transition: opacity 0.3s ease;
|
44 |
+
z-index: 2;
|
45 |
+
}
|
46 |
+
|
47 |
+
.film-card:hover .film-card-overlay {
|
48 |
+
opacity: 1;
|
49 |
+
}
|
50 |
+
|
51 |
+
.film-card-info {
|
52 |
+
height: 100%;
|
53 |
+
padding: 10px;
|
54 |
+
background-color: rgba(0, 0, 0, 0.807);
|
55 |
+
border-top: 1px solid #333;
|
56 |
+
z-index: 3;
|
57 |
+
position: relative;
|
58 |
+
}
|
59 |
+
|
60 |
+
@import url('https://fonts.googleapis.com/css2?family=Calistoga&family=Pacifico&family=Rubik+Burned&family=Rubik+Marker+Hatch&family=Rubik+Maze&family=Rubik+Microbe&family=Rubik:ital,wght@0,300..900;1,300..900&family=Signika:[email protected]&display=swap');
|
61 |
+
|
62 |
+
.film-card-title {
|
63 |
+
margin: 0;
|
64 |
+
font-family: "Signika", sans-serif;
|
65 |
+
font-optical-sizing: auto;
|
66 |
+
font-style: normal;
|
67 |
+
font-variation-settings:
|
68 |
+
"GRAD" 0;
|
69 |
+
font-weight: 600;
|
70 |
+
color: #f5f5f5;
|
71 |
+
width: 100%;
|
72 |
+
box-sizing: border-box;
|
73 |
+
text-align: center;
|
74 |
+
white-space: nowrap;
|
75 |
+
overflow: hidden;
|
76 |
+
text-overflow: ellipsis;
|
77 |
+
}
|
78 |
+
|
79 |
+
.film-card-year {
|
80 |
+
margin: 0;
|
81 |
+
font-size: .9em;
|
82 |
+
font-family: "Signika", sans-serif;
|
83 |
+
font-optical-sizing: auto;
|
84 |
+
font-style: normal;
|
85 |
+
font-variation-settings:
|
86 |
+
"GRAD" 0;
|
87 |
+
font-weight: 600;
|
88 |
+
color: #949494;
|
89 |
+
border: none;
|
90 |
+
width: 100%;
|
91 |
+
box-sizing: border-box;
|
92 |
+
text-align: center;
|
93 |
+
white-space: nowrap;
|
94 |
+
overflow: hidden;
|
95 |
+
text-overflow: ellipsis;
|
96 |
+
}
|
97 |
+
|
98 |
+
.spinner {
|
99 |
+
display: flex;
|
100 |
+
justify-content: center;
|
101 |
+
align-items: center;
|
102 |
+
margin: auto;
|
103 |
+
}
|
104 |
+
|
105 |
+
.spinner div {
|
106 |
+
width: 15px;
|
107 |
+
height: 15px;
|
108 |
+
background-color: #ff8c00;
|
109 |
+
border-radius: 50%;
|
110 |
+
animation: spin 1s infinite ease-in-out;
|
111 |
+
margin: 0 5px;
|
112 |
+
}
|
113 |
+
|
114 |
+
.spinner div:nth-child(1) {
|
115 |
+
animation-delay: -0.32s;
|
116 |
+
}
|
117 |
+
|
118 |
+
.spinner div:nth-child(2) {
|
119 |
+
animation-delay: -0.16s;
|
120 |
+
}
|
121 |
+
|
122 |
+
@keyframes spin {
|
123 |
+
0%, 100% {
|
124 |
+
transform: translateY(0);
|
125 |
+
}
|
126 |
+
50% {
|
127 |
+
transform: translateY(-20px);
|
128 |
+
}
|
129 |
+
}
|
130 |
+
|
131 |
+
.film-card.loading {
|
132 |
+
display: flex;
|
133 |
+
flex-direction: column;
|
134 |
+
justify-content: center;
|
135 |
+
align-items: center;
|
136 |
+
text-align: center;
|
137 |
+
background-color: #2c2c2c;
|
138 |
+
border: 1px solid #333;
|
139 |
+
border-radius: 15px;
|
140 |
+
padding: 20px;
|
141 |
+
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5);
|
142 |
+
width: 250px;
|
143 |
+
height: 420px;
|
144 |
+
position: relative;
|
145 |
+
}
|
146 |
+
|
147 |
+
.film-card.loading::before {
|
148 |
+
content: '';
|
149 |
+
position: absolute;
|
150 |
+
top: 0;
|
151 |
+
left: 0;
|
152 |
+
width: 250px;
|
153 |
+
height: 420px;
|
154 |
+
background: rgba(0, 0, 0, 0.5);
|
155 |
+
border-radius: 15px;
|
156 |
+
z-index: 1;
|
157 |
+
}
|
158 |
+
|
159 |
+
.film-card.loading .spinner {
|
160 |
+
margin-bottom: 20px;
|
161 |
+
}
|
162 |
+
|
163 |
+
.film-card.loading .film-card-title {
|
164 |
+
font-size: 1em;
|
165 |
+
color: #f5f5f5;
|
166 |
+
width: 100%;
|
167 |
+
text-align: center;
|
168 |
+
z-index: 2;
|
169 |
+
margin-top: 10px;
|
170 |
+
}
|
171 |
+
|
172 |
+
/* Media Queries for Mobile Devices */
|
173 |
+
@media (max-width: 768px) {
|
174 |
+
.film-card {
|
175 |
+
width: 150px;
|
176 |
+
height: 265px;
|
177 |
+
margin: 10px 0;
|
178 |
+
}
|
179 |
+
.film-card.loading{
|
180 |
+
width: 150px;
|
181 |
+
height: 265px;
|
182 |
+
margin: 10px 0;
|
183 |
+
}
|
184 |
+
|
185 |
+
.film-card-info {
|
186 |
+
padding: 5px;
|
187 |
+
}
|
188 |
+
|
189 |
+
.film-card-title, .film-card-year {
|
190 |
+
font-size: 0.8em;
|
191 |
+
|
192 |
+
}
|
193 |
+
}
|
frontend/src/components/film/card.js
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect, useState } from "react";
|
2 |
+
import { useRouter } from 'next/navigation'; // Use the correct import for Next.js 13
|
3 |
+
import apiClient from "@/api/apiClient";
|
4 |
+
import './card.css';
|
5 |
+
|
6 |
+
// Spinner component
|
7 |
+
const Spinner = () => (
|
8 |
+
<div className="spinner">
|
9 |
+
<div className="spinner-bounce1"></div>
|
10 |
+
<div className="spinner-bounce2"></div>
|
11 |
+
<div className="spinner-bounce3"></div>
|
12 |
+
</div>
|
13 |
+
);
|
14 |
+
|
15 |
+
export default function FilmCard({ title }) {
|
16 |
+
const [metadata, setMetadata] = useState(null);
|
17 |
+
const router = useRouter(); // Use Next.js router
|
18 |
+
|
19 |
+
useEffect(() => {
|
20 |
+
const fetchMetadata = async () => {
|
21 |
+
try {
|
22 |
+
const response = await apiClient.getFilmMetadataByTitle(title);
|
23 |
+
const filmData = response.data; // Adjust based on actual API response
|
24 |
+
console.log(filmData);
|
25 |
+
setMetadata(filmData);
|
26 |
+
} catch (error) {
|
27 |
+
console.error("Failed to fetch metadata:", error);
|
28 |
+
setMetadata({ error: "Failed to load metadata" });
|
29 |
+
}
|
30 |
+
};
|
31 |
+
|
32 |
+
fetchMetadata();
|
33 |
+
}, [title]);
|
34 |
+
|
35 |
+
const handleCardClick = () => {
|
36 |
+
router.push(`/film/${title}`); // Use Next.js router
|
37 |
+
};
|
38 |
+
|
39 |
+
const findEnglishTranslation = (translations) => {
|
40 |
+
if (translations && Array.isArray(translations.nameTranslations)) {
|
41 |
+
const primaryEngTranslation = translations.nameTranslations.find(
|
42 |
+
translation => translation.language === 'eng' && translation.isPrimary
|
43 |
+
);
|
44 |
+
|
45 |
+
if (primaryEngTranslation) {
|
46 |
+
return primaryEngTranslation.name;
|
47 |
+
}
|
48 |
+
|
49 |
+
const engTranslation = translations.nameTranslations.find(
|
50 |
+
translation => translation.language === 'eng'
|
51 |
+
);
|
52 |
+
|
53 |
+
if (engTranslation) {
|
54 |
+
return engTranslation.name;
|
55 |
+
}
|
56 |
+
}
|
57 |
+
return null;
|
58 |
+
};
|
59 |
+
|
60 |
+
const eng_title = metadata?.translations ?
|
61 |
+
(findEnglishTranslation(metadata.translations) || `${metadata?.name} (${metadata?.year})`) :
|
62 |
+
`${metadata?.name} (${metadata?.year})` || title;
|
63 |
+
|
64 |
+
const imageUrl = metadata?.artworks?.find(artwork => artwork.type === 14)?.thumbnail || "";
|
65 |
+
const year = metadata?.year;
|
66 |
+
|
67 |
+
if (metadata === null)
|
68 |
+
return (
|
69 |
+
<div className="film-card loading">
|
70 |
+
<Spinner />
|
71 |
+
<div className="film-card-title">Loading...</div>
|
72 |
+
</div>
|
73 |
+
);
|
74 |
+
|
75 |
+
if (metadata.error)
|
76 |
+
return (
|
77 |
+
<div className="film-card error">
|
78 |
+
<p className="film-card-title">Error loading metadata</p>
|
79 |
+
</div>
|
80 |
+
);
|
81 |
+
|
82 |
+
return (
|
83 |
+
<div className="film-card" onClick={handleCardClick}>
|
84 |
+
<img src={imageUrl} alt={eng_title} className="film-card-image" />
|
85 |
+
<div className="film-card-overlay"></div>
|
86 |
+
<div className="film-card-info">
|
87 |
+
<p className="film-card-title">{eng_title}</p>
|
88 |
+
<p className="film-card-year">{year}</p>
|
89 |
+
</div>
|
90 |
+
</div>
|
91 |
+
);
|
92 |
+
}
|
frontend/src/context/FilmContext.js
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { createContext, useState, useContext } from 'react';
|
4 |
+
|
5 |
+
const FilmContext = createContext();
|
6 |
+
|
7 |
+
export const useFilmContext = () => useContext(FilmContext);
|
8 |
+
|
9 |
+
export const FilmProvider = ({ children }) => {
|
10 |
+
const [films, setFilms] = useState([]);
|
11 |
+
|
12 |
+
return (
|
13 |
+
<FilmContext.Provider value={{ films, setFilms }}>
|
14 |
+
{children}
|
15 |
+
</FilmContext.Provider>
|
16 |
+
);
|
17 |
+
};
|
frontend/src/context/TvshowContext.js
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
import React, { createContext, useContext, useState } from 'react';
|
3 |
+
|
4 |
+
const TvShowsContext = createContext();
|
5 |
+
|
6 |
+
export const TvShowsProvider = ({ children }) => {
|
7 |
+
const [tvshows, setTvshows] = useState([]);
|
8 |
+
|
9 |
+
return (
|
10 |
+
<TvShowsContext.Provider value={{ tvshows, setTvshows }}>
|
11 |
+
{children}
|
12 |
+
</TvShowsContext.Provider>
|
13 |
+
);
|
14 |
+
};
|
15 |
+
|
16 |
+
export const useTvShowsContext = () => {
|
17 |
+
return useContext(TvShowsContext);
|
18 |
+
};
|
frontend/src/lib/LoadBalancer.js
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class LoadBalancerAPI {
|
2 |
+
constructor(baseURL) {
|
3 |
+
this.baseURL = baseURL;
|
4 |
+
this.filmCache = null; // Cache for film store
|
5 |
+
this.tvCache = null; // Cache for TV store
|
6 |
+
}
|
7 |
+
|
8 |
+
async getInstances() {
|
9 |
+
return this._getRequest('/api/get/instances');
|
10 |
+
}
|
11 |
+
|
12 |
+
async getInstancesHealth() {
|
13 |
+
return this._getRequest('/api/get/instances/health');
|
14 |
+
}
|
15 |
+
|
16 |
+
async getMovieByTitle(title) {
|
17 |
+
return this._getRequest(`/api/get/film/${encodeURIComponent(title)}`);
|
18 |
+
}
|
19 |
+
|
20 |
+
async getTVEpisode(title, season, episode) {
|
21 |
+
return this._getRequest(`/api/get/tv/${encodeURIComponent(title)}/${season}/${episode}`);
|
22 |
+
}
|
23 |
+
|
24 |
+
async getTVStore() {
|
25 |
+
const response = await this._getRequest('/api/get/tv/store');
|
26 |
+
|
27 |
+
if (response && Object.keys(response).length > 0) {
|
28 |
+
this.tvCache = response; // Update cache if response is not empty
|
29 |
+
}
|
30 |
+
|
31 |
+
return this.tvCache || {}; // Return cache if response is empty
|
32 |
+
}
|
33 |
+
|
34 |
+
async getFilmStore() {
|
35 |
+
const response = await this._getRequest('/api/get/film/store');
|
36 |
+
|
37 |
+
if (response && Object.keys(response).length > 0) {
|
38 |
+
this.filmCache = response; // Update cache if response is not empty
|
39 |
+
}
|
40 |
+
|
41 |
+
return this.filmCache || {}; // Return cache if response is empty
|
42 |
+
}
|
43 |
+
|
44 |
+
async getFilmMetadataByTitle(title) {
|
45 |
+
return this._getRequest(`/api/get/film/metadata/${encodeURIComponent(title)}`);
|
46 |
+
}
|
47 |
+
|
48 |
+
async getTVMetadataByTitle(title) {
|
49 |
+
return this._getRequest(`/api/get/tv/metadata/${encodeURIComponent(title)}`);
|
50 |
+
}
|
51 |
+
|
52 |
+
async getSeasonMetadataBySeriesId(seires_id, season) {
|
53 |
+
return this._getRequest(`/api/get/tv/metadata/${seires_id}/${season}`);
|
54 |
+
}
|
55 |
+
|
56 |
+
async getAllFilms() {
|
57 |
+
return this._getRequest('/api/get/film/all');
|
58 |
+
}
|
59 |
+
|
60 |
+
async getAllTVShows() {
|
61 |
+
return this._getRequest('/api/get/tv/all');
|
62 |
+
}
|
63 |
+
|
64 |
+
async getDownloadProgress(url) {
|
65 |
+
return this._getRequestNoBase(url);
|
66 |
+
}
|
67 |
+
|
68 |
+
// Helper methods for GET and POST requests
|
69 |
+
async _getRequest(endpoint) {
|
70 |
+
try {
|
71 |
+
const response = await fetch(`${this.baseURL}${endpoint}`, {
|
72 |
+
method: 'GET',
|
73 |
+
headers: { 'Content-Type': 'application/json' },
|
74 |
+
});
|
75 |
+
console.log(`api endpoint: ${this.baseURL}${endpoint}`);
|
76 |
+
return await this._handleResponse(response);
|
77 |
+
} catch (error) {
|
78 |
+
console.error(`Error during GET request to ${endpoint}:`, error);
|
79 |
+
throw error;
|
80 |
+
}
|
81 |
+
}
|
82 |
+
|
83 |
+
async _postRequest(endpoint, body) {
|
84 |
+
try {
|
85 |
+
const response = await fetch(`${this.baseURL}${endpoint}`, {
|
86 |
+
method: 'POST',
|
87 |
+
headers: { 'Content-Type': 'application/json' },
|
88 |
+
body: JSON.stringify(body),
|
89 |
+
});
|
90 |
+
return await this._handleResponse(response);
|
91 |
+
} catch (error) {
|
92 |
+
console.error(`Error during POST request to ${endpoint}:`, error);
|
93 |
+
throw error;
|
94 |
+
}
|
95 |
+
}
|
96 |
+
|
97 |
+
async _handleResponse(response) {
|
98 |
+
if (!response.ok) {
|
99 |
+
const errorDetails = await response.text();
|
100 |
+
throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`);
|
101 |
+
}
|
102 |
+
return response.json();
|
103 |
+
}
|
104 |
+
|
105 |
+
async _getRequestNoBase(url) {
|
106 |
+
try {
|
107 |
+
const response = await fetch(`${url}`, {
|
108 |
+
method: 'GET',
|
109 |
+
headers: { 'Content-Type': 'application/json' },
|
110 |
+
});
|
111 |
+
console.log(`api endpoint: ${url}`);
|
112 |
+
return await this._handleResponse(response);
|
113 |
+
} catch (error) {
|
114 |
+
console.error(`Error during GET request to ${url}:`, error);
|
115 |
+
throw error;
|
116 |
+
}
|
117 |
+
}
|
118 |
+
}
|
119 |
+
|
120 |
+
export { LoadBalancerAPI };
|
frontend/src/lib/config.js
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const config = {
|
2 |
+
apiBaseUrl: 'https://hans-den-load-balancer.hf.space',
|
3 |
+
searchUrl: 'https://unicone-studio-search.hf.space/api/search',
|
4 |
+
version: "0.0.0.1 Alpha",
|
5 |
+
};
|
6 |
+
|
7 |
+
export default config;
|