Compare commits

...

7 Commits

Author SHA1 Message Date
7f728b821e Update README to include installation instructions for React with Vite and TypeScript, and optional tools like TailwindCSS and DaisyUI 2025-02-09 13:04:36 +01:00
0ad0534b22 Add method to send progress updates to caller
Introduced a `SendProgressUpdate` method in `WeatherUpdateHub` to allow sending progress messages specifically to the calling client. This enhances client feedback during operations.
2025-02-09 02:04:50 +01:00
c4071786ea Implement SignalR integration and refactor WeatherPage
Added SignalR for real-time progress updates during weather data fetch. Refactored WeatherPage to use a new reusable WeatherGrid component and SignalRHelper. Improved loading UI with a radial progress indicator.
2025-02-09 02:00:06 +01:00
72b7902b55 Remove unused React logo SVG and add new API client and layouts
The React logo SVG file was removed as it was no longer needed. Introduced an auto-generated API client class (`ApiClient.ts`) for handling server communication and added new components for the navigation menu and higher-order layouts (`NavMenu`, `with_main_layout.ts.tsx`). Updated `yarn.lock` to replace legacy URL references.
2025-02-09 00:00:05 +01:00
7b1c4c27fe Refactor dark mode handling and update UI components
Extract dark mode logic into a reusable helper function to improve code clarity and maintainability. Updated the UI layout for better structure and styling, including replacing placeholder logos and enhancing card design.
2025-02-08 00:22:20 +01:00
9b45752d1a **Initialize React Vite project with basic setup**
Add initial project structure including React, Vite, TailwindCSS, and Syncfusion dependencies. Configure tooling with ESLint, TypeScript, and Tailwind plugins. Set up `.gitignore`, Syncfusion themes, and example components for demonstration purposes.
2025-02-07 23:21:46 +01:00
be2599218d Add SignalR integration for real-time weather updates
Implemented SignalR to enable real-time communication between the server and connected clients. Added a new hub (`WeatherUpdateHub`), a background service (`SignalRSendService`), and modified both the API backend and React frontend for seamless message broadcasting and handling.
2025-02-06 22:21:53 +01:00
54 changed files with 7680 additions and 75 deletions

13
.idea/.idea.ASPReactDemo/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/projectSettingsUpdater.xml
/contentModel.xml
/modules.xml
/.idea.ASPReactDemo.iml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxBlameSettings">
<option name="version" value="2" />
</component>
</project>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

6
.idea/.idea.ASPReactDemo/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="NSwag.AspNetCore" Version="14.2.0" />
</ItemGroup>

View File

@@ -1,20 +1,24 @@
using Api.SignalR;
using Microsoft.AspNetCore.SignalR;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();
builder.Services.AddOpenApiDocument(config =>
{
config.Title = "NSwag Demo API";
});
builder.Services.AddSignalR();
builder.Services.AddHostedService<SignalRSendService>();
builder.Services.AddSingleton<IUserIdProvider, UserProvider>();
builder.Services.AddOpenApiDocument(config => { config.Title = "NSwag Demo API"; });
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
policy.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
@@ -24,8 +28,8 @@ var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseOpenApi();
app.UseSwaggerUi();
app.UseOpenApi();
app.UseSwaggerUi();
}
app.UseHttpsRedirection();
@@ -36,23 +40,36 @@ var summaries = new[]
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.MapGet("/weatherforecast", async () =>
{
var hub = app.Services.GetRequiredService<IHubContext<WeatherUpdateHub>>();
for (var i = 0; i < 100; i++)
{
await hub.Clients.All.SendAsync("ProgressUpdate", "None", $$"""{"percentage": "{{i}}", "message": "{{DateTime.Now.Millisecond}}"}""");
await Task.Delay(100);
}
var forecast = Enumerable.Range(1, 2500).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
// Add SignalR hub
app.MapHub<WeatherUpdateHub>("/WeatherUpdateHub");
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.SignalR;
namespace Api.SignalR
{
public class SignalRSendService(IHubContext<WeatherUpdateHub> hubContext, ILogger<SignalRSendService> logger) : BackgroundService
{
#region Override ExecuteAsync
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// For example, wait 10 seconds between messages.
await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);
// Log or do any work here.
logger.LogInformation("Background service sending message at: {Time}", DateTime.Now);
// Send a message to all connected clients.
await hubContext.Clients.All.SendAsync(
"ReceiveMessage", // This is the client method name.
"Background Service", // Example sender.
$"Hello from the background task at {DateTime.Now:F}", // The message.
stoppingToken);
}
}
#endregion
}
}

View File

@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.SignalR;
namespace Api.SignalR
{
public class UserProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
// Extract the "userId" query parameter from the HTTP context.
var httpContext = connection.GetHttpContext();
return httpContext?.Request.Query["userId"].FirstOrDefault();
}
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.SignalR;
namespace Api.SignalR
{
public class WeatherUpdateHub: Hub
{
// This method can be called by connected clients.
public async Task SendMessage(string user, string message)
{
// Broadcast the message to all connected clients.
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public async Task SendProgressUpdate(string message)
{
// Send a message to the caller.
await Clients.Caller.SendAsync("ProgressUpdate", message);
}
}
}

111
README.md
View File

@@ -1,59 +1,92 @@
# ASPReactDemo
<h1> ASP Net Core, Nswag, SignalR, React, Syncfusion </h1>
Eine Demo für:
-ASP Net Core
-Nswag
-SignalR
-React
-Syncfusion
- [Installation React with Vite and TypeScript](#installation-react-with-vite-and-typescript)
- [Routing](#routing)
- [Install react-router-dom](#install-react-router-dom)
- [Update App.tsx](#update-apptsx)
- [Update configuration files](#update-configuration-files)
- [Update css file](#update-css-file)
- [DaisyUI 5](#daisyui-5)
- [Install daisyUI](#install-daisyui)
- [Update css file](#update-css-file-1)
## Installation React
### With Yarn
# Installation React with Vite and TypeScript
```bash
yarn create react-app react-client --template typescript
# Create a new project using the "react-ts" template
yarn create vite my-react-app --template react-ts
cd react-client
# Navigate to the project folder
cd my-react-app
yarn build
# Install dependencies
yarn install
yarn start
```
## Installation tailwindcss
### Install and Init
# Routing
## Install react-router-dom
```bash
yarn add tailwindcss postcss autoprefixer
yarn add react-router-dom
```
npx tailwindcss init -p
## Update App.tsx
```tsx
# Optional Tools
## tailwindcss
### Install tailwindcss
```bash
# Install tailwindcss
yarn add tailwindcss@latest postcss@latest autoprefixer@latest
```
### Update configuration files
```typescript
# vite.config.ts
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite' // Add this line
export default defineConfig({
plugins: [
tailwindcss(), //Add this line
],
})
```
### Configure the tailwind.config.js File
Update the tailwind.config.js file to specify the paths to all of your template files. Replace the default content with the following (if not already present):
```javascript
module.exports = {
purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
```
### Add Tailwind directives to your CSS:
### Update css file
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
/* eg. src/index.css */
@import "tailwindcss";
/* Add your custom styles here */
```
## DaisyUI 5
### Install daisyUI
```bash
# Install daisyUI
yarn add -D daisyui@beta
```
### Update css file
```css
/* eg. src/index.css */
@import "tailwindcss"; /* Existing line */
@plugin "daisyui";
/* Add your custom styles here */
```

8
react-client/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

4
react-client/.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxBlameSettings">
<option name="version" value="2" />
</component>
</project>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

8
react-client/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/react-client.iml" filepath="$PROJECT_DIR$/.idea/react-client.iml" />
</modules>
</component>
</project>

12
react-client/.idea/react-client.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="start" type="js.build_tools.npm" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="start" />
</scripts>
<node-interpreter value="project" />
<envs />
<EXTENSION ID="com.intellij.lang.javascript.buildTools.npm.rc.StartBrowserRunConfigurationExtension">
<browser name="37cae5b9-e8b2-4949-9172-aafa37fbc09c" start="true" with-js-debugger="true" url="http://localhost:3000/" />
</EXTENSION>
<method v="2" />
</configuration>
</component>

6
react-client/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@microsoft/signalr": "^8.0.7",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",

View File

@@ -2,32 +2,61 @@ import React, { useEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';
import { Client, WeatherForecast } from './ApiCient';
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
function App() {
const [forecast, setForecast] = useState<WeatherForecast[] | undefined>([]);
const [connection, setConnection] = useState<HubConnection | null>(null);
const [message, setMessage] = useState<string>('');
useEffect(() => {
const client = new Client();
const newConnection = new HubConnectionBuilder()
.withUrl('http://localhost:5175/WeatherUpdateHub?userId=user1234')
.withAutomaticReconnect()
.build();
const fetchForecast = async () => {
const forecast = await client.getWeatherForecast();
setForecast(forecast);
};
fetchForecast();
const intervalId = setInterval(fetchForecast, 1000);
return () => clearInterval(intervalId);
setConnection(newConnection);
}, []);
useEffect(() => {
if (connection) {
connection
.start()
.then(() => {
console.log('Connected to SignalR hub!');
connection.on('ReceiveMessage', (user, message) => {
setMessage(message);
console.log('Received message:', user, message);
});
})
.catch(error => console.error('SignalR Connection Error: ', error));
}
}, [connection]);
// useEffect(() => {
// const client = new Client();
// const fetchForecast = async () => {
// const forecast = await client.getWeatherForecast();
// setForecast(forecast);
// };
// fetchForecast();
// const intervalId = setInterval(fetchForecast, 1000);
// return () => clearInterval(intervalId);
// }, []);
return (
<div className="App">
{/* <header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
</div> <p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
@@ -49,6 +78,10 @@ function App() {
setForecast(forecast);
}} > Call API</button>
<div className="text-red-50">
{message}
</div>
{forecast?.map((item: WeatherForecast) => {
return (

View File

@@ -1557,6 +1557,17 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1"
integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==
"@microsoft/signalr@^8.0.7":
version "8.0.7"
resolved "https://registry.yarnpkg.com/@microsoft/signalr/-/signalr-8.0.7.tgz#94419ddbf9418753e493f4ae4c13990316ec2ea5"
integrity sha512-PHcdMv8v5hJlBkRHAuKG5trGViQEkPYee36LnJQx4xHOQ5LL4X0nEWIxOp5cCtZ7tu+30quz5V3k0b1YNuc6lw==
dependencies:
abort-controller "^3.0.0"
eventsource "^2.0.2"
fetch-cookie "^2.0.3"
node-fetch "^2.6.7"
ws "^7.4.5"
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1"
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
@@ -2639,6 +2650,13 @@ abab@^2.0.3, abab@^2.0.5:
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies:
event-target-shim "^5.0.0"
accepts@~1.3.4, accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
@@ -4712,6 +4730,11 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
eventemitter3@^4.0.0:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
@@ -4722,6 +4745,11 @@ events@^3.2.0:
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
eventsource@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-2.0.2.tgz#76dfcc02930fb2ff339520b6d290da573a9e8508"
integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==
execa@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@@ -4852,6 +4880,14 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
fetch-cookie@^2.0.3:
version "2.2.0"
resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-2.2.0.tgz#01086b6b5b1c3e08f15ffd8647b02ca100377365"
integrity sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==
dependencies:
set-cookie-parser "^2.4.8"
tough-cookie "^4.0.0"
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@@ -7069,6 +7105,13 @@ node-addon-api@^7.0.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
node-fetch@^2.6.7:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
dependencies:
whatwg-url "^5.0.0"
node-forge@^1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
@@ -8769,6 +8812,11 @@ serve-static@1.16.2:
parseurl "~1.3.3"
send "0.19.0"
set-cookie-parser@^2.4.8:
version "2.7.1"
resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943"
integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==
set-function-length@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
@@ -9480,6 +9528,11 @@ tr46@^2.1.0:
dependencies:
punycode "^2.1.1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
tryer@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
@@ -9799,6 +9852,11 @@ web-vitals@^2.1.0:
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.4.tgz#76563175a475a5e835264d373704f9dde718290c"
integrity sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
@@ -9950,6 +10008,14 @@ whatwg-mimetype@^2.3.0:
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
whatwg-url@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
@@ -10250,7 +10316,7 @@ write-file-atomic@^3.0.0:
signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5"
ws@^7.4.6:
ws@^7.4.5, ws@^7.4.6:
version "7.5.10"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==

24
react-vite-client/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="build" type="js.build_tools.npm" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="build" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="dev" type="js.build_tools.npm" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<EXTENSION ID="com.intellij.lang.javascript.buildTools.npm.rc.StartBrowserRunConfigurationExtension">
<browser name="37cae5b9-e8b2-4949-9172-aafa37fbc09c" start="true" with-js-debugger="true" url="http://localhost:5173/" />
</EXTENSION>
<method v="2">
<option name="RunConfigurationTask" enabled="true" run_configuration_name="build" run_configuration_type="js.build_tools.npm" />
</method>
</configuration>
</component>

View File

@@ -0,0 +1,50 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

View File

@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/src/styles.css" rel="stylesheet">
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,73 @@
{
"runtime": "Net90",
"defaultVariables": null,
"documentGenerator": {
"fromDocument": {
"url": "http://localhost:5175/swagger/v1/swagger.json",
"output": null,
"newLineBehavior": "Auto"
}
},
"codeGenerators": {
"openApiToTypeScriptClient": {
"className": "{controller}Client",
"moduleName": "",
"namespace": "",
"typeScriptVersion": 4.0,
"template": "Fetch",
"promiseType": "Promise",
"httpClass": "HttpClient",
"withCredentials": false,
"useSingletonProvider": false,
"injectionTokenType": "OpaqueToken",
"rxJsVersion": 6.0,
"dateTimeType": "Date",
"nullValue": "Undefined",
"generateClientClasses": true,
"generateClientInterfaces": false,
"generateOptionalParameters": true,
"exportTypes": true,
"wrapDtoExceptions": false,
"exceptionClass": "SwaggerException",
"clientBaseClass": null,
"wrapResponses": false,
"wrapResponseMethods": [],
"generateResponseClasses": true,
"responseClass": "SwaggerResponse",
"protectedMethods": [],
"configurationClass": null,
"useTransformOptionsMethod": false,
"useTransformResultMethod": false,
"generateDtoTypes": true,
"operationGenerationMode": "MultipleClientsFromOperationId",
"markOptionalProperties": true,
"generateCloneMethod": false,
"typeStyle": "Class",
"enumStyle": "Enum",
"useLeafType": false,
"classTypes": [],
"extendedClasses": [],
"extensionCode": null,
"generateDefaultValues": true,
"excludedTypeNames": [],
"excludedParameterNames": [],
"handleReferences": false,
"generateTypeCheckFunctions": false,
"generateConstructorInterface": true,
"convertConstructorInterfaceData": false,
"importRequiredTypes": true,
"useGetBaseUrlMethod": false,
"baseUrlTokenName": "API_BASE_URL",
"queryNullValue": "",
"useAbortSignal": false,
"inlineNamedDictionaries": false,
"inlineNamedAny": false,
"includeHttpContext": false,
"templateDirectory": null,
"serviceHost": null,
"serviceSchemes": null,
"output": "src/ApiCient.ts",
"newLineBehavior": "Auto"
}
}
}

4541
react-vite-client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
{
"name": "react-vite-client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@syncfusion/ej2-react-grids": "^28.2.4",
"@tailwindcss/vite": "^4.0.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.5",
"tailwindcss": "^4.0.4",
"use-local-storage-state": "^19.5.0"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"daisyui": "^5.0.0-beta.7",
"eslint": "^9.19.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.22.0",
"vite": "^6.1.0"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,146 @@
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------
/* tslint:disable */
/* eslint-disable */
// ReSharper disable InconsistentNaming
export class Client {
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
this.http = http ? http : window as any;
this.baseUrl = baseUrl ?? "http://localhost:5175";
}
getWeatherForecast(): Promise<WeatherForecast[]> {
let url_ = this.baseUrl + "/weatherforecast";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processGetWeatherForecast(_response);
});
}
protected processGetWeatherForecast(response: Response): Promise<WeatherForecast[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
if (Array.isArray(resultData200)) {
result200 = [] as any;
for (let item of resultData200)
result200!.push(WeatherForecast.fromJS(item));
}
else {
result200 = <any>null;
}
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<WeatherForecast[]>(null as any);
}
}
export class WeatherForecast implements IWeatherForecast {
date?: Date;
temperatureC?: number;
summary?: string | undefined;
temperatureF?: number;
constructor(data?: IWeatherForecast) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.date = _data["date"] ? new Date(_data["date"].toString()) : <any>undefined;
this.temperatureC = _data["temperatureC"];
this.summary = _data["summary"];
this.temperatureF = _data["temperatureF"];
}
}
static fromJS(data: any): WeatherForecast {
data = typeof data === 'object' ? data : {};
let result = new WeatherForecast();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["date"] = this.date ? formatDate(this.date) : <any>undefined;
data["temperatureC"] = this.temperatureC;
data["summary"] = this.summary;
data["temperatureF"] = this.temperatureF;
return data;
}
}
export interface IWeatherForecast {
date?: Date;
temperatureC?: number;
summary?: string | undefined;
temperatureF?: number;
}
function formatDate(d: Date) {
return d.getFullYear() + '-' +
(d.getMonth() < 9 ? ('0' + (d.getMonth()+1)) : (d.getMonth()+1)) + '-' +
(d.getDate() < 10 ? ('0' + d.getDate()) : d.getDate());
}
export class SwaggerException extends Error {
message: string;
status: number;
response: string;
headers: { [key: string]: any; };
result: any;
constructor(message: string, status: number, response: string, headers: { [key: string]: any; }, result: any) {
super();
this.message = message;
this.status = status;
this.response = response;
this.headers = headers;
this.result = result;
}
protected isSwaggerException = true;
static isSwaggerException(obj: any): obj is SwaggerException {
return obj.isSwaggerException === true;
}
}
function throwException(message: string, status: number, response: string, headers: { [key: string]: any; }, result?: any): any {
if (result !== null && result !== undefined)
throw result;
else
throw new SwaggerException(message, status, response, headers, null);
}

View File

View File

@@ -0,0 +1,22 @@
// App.tsx
import React from 'react';
import {BrowserRouter as Router, Route, Routes} from 'react-router-dom';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import WeatherPage from "./pages/WeatherPage/WeatherPage.tsx";
const App: React.FC = () => {
return (
<Router>
<Routes>
<Route path="/" element={<HomePage/>}/>
<Route path="/about" element={<AboutPage/>}/>
<Route path="/weather" element={<WeatherPage/>}/>
{/* Other routes */}
</Routes>
</Router>
);
};
export default App;

View File

@@ -0,0 +1,19 @@
@import "tailwindcss";
@import "../node_modules/@syncfusion/ej2-base/styles/tailwind3.css";
@import "../node_modules/@syncfusion/ej2-buttons/styles/tailwind3.css";
@import "../node_modules/@syncfusion/ej2-calendars/styles/tailwind3.css";
@import "../node_modules/@syncfusion/ej2-dropdowns/styles/tailwind3.css";
@import "../node_modules/@syncfusion/ej2-inputs/styles/tailwind3.css";
@import "../node_modules/@syncfusion/ej2-navigations/styles/tailwind3.css";
@import "../node_modules/@syncfusion/ej2-popups/styles/tailwind3.css";
@import "../node_modules/@syncfusion/ej2-splitbuttons/styles/tailwind3.css";
@import "../node_modules/@syncfusion/ej2-react-grids/styles/tailwind3.css";
@plugin "daisyui" {
themes: light --default, dark --prefersdark;
}

View File

@@ -0,0 +1,110 @@
// Layout.tsx
import React, {useEffect, useState} from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faBars} from '@fortawesome/free-solid-svg-icons';
import {handleDarkMode} from "../theme_helpers/ThemeHelpers";
import NavMenu from "./NavMenue.tsx";
interface LayoutProps {
children: React.ReactNode;
}
const Layout: React.FC<LayoutProps> = ({children}) => {
// Determine if we are on mobile (below 768px)
const [isMobile, setIsMobile] = useState<boolean>(() => {
if (typeof window !== 'undefined') {
return window.innerWidth < 768;
}
return false;
});
// Sidebar open state: default open on desktop, closed on mobile.
const [isSidebarOpen, setSidebarOpen] = useState<boolean>(() => {
if (typeof window !== 'undefined') {
return window.innerWidth >= 768;
}
return false;
});
// Setup dark mode theme.
useEffect(() => {
return handleDarkMode();
}, []);
// Update mobile flag and sidebar state on resize.
useEffect(() => {
const handleResize = () => {
const mobile = window.innerWidth < 768;
setIsMobile(mobile);
// Set sidebar open to false on mobile, true on desktop.
setSidebarOpen(!mobile);
};
window.addEventListener('resize', handleResize);
// Initial check.
handleResize();
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div className="flex flex-col min-h-screen relative">
{/* Header */}
<header className="bg-primary text-primary-content p-4 flex">
<div className="container flex">
<button
className="btn btn-square btn-ghost mr-4"
onClick={() => setSidebarOpen(!isSidebarOpen)}
>
<FontAwesomeIcon icon={faBars} className="fa-2xl"/>
</button>
<h1 className="text-3xl font-bold">My React Site</h1>
</div>
</header>
{/* For mobile, add an overlay backdrop when the sidebar is open.
The overlay now starts below the header (top-16) */}
{isMobile && isSidebarOpen && (
<div
className="fixed top-16 left-0 right-0 bottom-0 bg-black opacity-50 z-40"
onClick={() => setSidebarOpen(false)}
></div>
)}
{/* Content Area */}
<div className="flex flex-1">
{/* Sidebar Navigation */}
<aside className={`bg-base-200 transition-transform duration-300
${isMobile
? "fixed top-16 left-0 bottom-0 w-44 max-w-xs p-3 z-50"
: "min-w-36 max-w-52 w-1/5"
}
${isMobile
? (isSidebarOpen ? "translate-x-0" : "-translate-x-full")
: (isSidebarOpen ? "block" : "hidden")
}
`}
>
<NavMenu/>
</aside>
{/* Main Content */}
<main className="flex-grow container mx-auto p-4">
{children}
</main>
</div>
{/* Footer */}
<footer className="bg-neutral text-neutral-content p-4">
<div className="container mx-auto text-center">
<p>&copy; 2025 My React Site. All rights reserved.</p>
</div>
</footer>
</div>
);
};
export default Layout;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import {Link} from 'react-router-dom';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCloudSun, faConciergeBell, faEnvelope, faHouse, faInfoCircle} from '@fortawesome/free-solid-svg-icons';
const NavMenu: React.FC = () => {
return (
<ul className="menu gap-y-1">
<li className="flex">
<Link to="/">
<FontAwesomeIcon icon={faHouse}/>
Home
</Link>
</li>
<li className="flex">
<Link to="/weather">
<FontAwesomeIcon icon={faCloudSun}/>
Weather
</Link>
</li>
<li className="flex">
<Link to="/about">
<FontAwesomeIcon icon={faInfoCircle}/>
About
</Link>
</li>
<li className="flex">
<Link to="/services">
<FontAwesomeIcon icon={faConciergeBell}/>
Services
</Link>
</li>
<li className="flex">
<Link to="/contact">
<FontAwesomeIcon icon={faEnvelope}/>
Contact
</Link>
</li>
</ul>
);
};
export default NavMenu;

View File

@@ -0,0 +1,15 @@
// withLayout.tsx
import React from 'react';
import Layout from './MainLayout';
const withLayout = <P extends object>(
WrappedComponent: React.ComponentType<P>
): React.FC<P> => {
return (props: P) => (
<Layout>
<WrappedComponent {...props} />
</Layout>
);
};
export default withLayout;

View File

@@ -0,0 +1,15 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { registerLicense } from '@syncfusion/ej2-base';
// Registering Syncfusion license key
registerLicense('Ngo9BigBOggjHTQxAR8/V1NMaF5cXmBCf1FpRmJGdld5fUVHYVZUTXxaS00DNHVRdkdmWX1ed3VTR2NYU010XkI=');
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,23 @@
// AboutPage.tsx
import React from 'react';
import withLayout from '../layouts/with_main_layout.ts.tsx'; // Adjust the path based on your project structure
const AboutPage: React.FC = () => {
return (
<div className="p-4">
<h2 className="text-2xl font-bold mb-4">About Us</h2>
<p className="mb-2">
Welcome to our website! We are committed to providing you with the best experience possible.
</p>
<p className="mb-2">
Our mission is to build high-quality applications that are fast, responsive, and user-friendly.
</p>
<p>
Whether you're here to learn more about our team or explore our services, we hope you enjoy your visit.
</p>
</div>
);
};
const NamedAboutPage = withLayout(AboutPage);
export default NamedAboutPage;

View File

@@ -0,0 +1,73 @@
import {useState} from 'react'
import '../App.css' // Ensure a declaration for CSS is added in a .d.ts file
import {ColumnDirective, ColumnsDirective, GridComponent} from '@syncfusion/ej2-react-grids';
import with_main_layout from '../layouts/with_main_layout.ts.tsx'; // adjust the path as necessary
function HomePage() {
const [count, setCount] = useState(0)
const data = [
{
OrderID: 10248, CustomerID: 'VINET', EmployeeID: 5, ShipCountry: 'France', Freight: 32.38
},
{
OrderID: 10249, CustomerID: 'TOMSP', EmployeeID: 6, ShipCountry: 'Germany', Freight: 11.61
},
{
OrderID: 10250, CustomerID: 'HANAR', EmployeeID: 4, ShipCountry: 'Brazil', Freight: 65.83
}
];
return (
<>
<h1>Vite, React, Syncfusion, Daisyui Demo</h1>
<div className="card shadow-md w-2/3 mt-5 bg-auto-50">
<div className="card-body">
<button className="btn w-full m-2" onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
</div>
<div className="badge badge-secondary m-4">primary</div>
<div role="alert" className="alert alert-error">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>Error! Task failed successfully.</span>
</div>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
<p className="font-bold underline">
Click on the Vite and React logos to learn more
</p>
<GridComponent dataSource={data}>
<ColumnsDirective>
<ColumnDirective field='OrderID' width='100' textAlign="Right"/>
<ColumnDirective field='CustomerID' width='100'/>
<ColumnDirective field='EmployeeID' width='100' textAlign="Right"/>
<ColumnDirective field='Freight' width='100' format="C2" textAlign="Right"/>
<ColumnDirective field='ShipCountry' width='100'/>
</ColumnsDirective>
</GridComponent>
</>
)
}
export const PageWithLayout = with_main_layout(HomePage)
export default PageWithLayout

View File

@@ -0,0 +1,33 @@
import React from "react";
import {ColumnDirective, ColumnsDirective, Filter, GridComponent, Inject, Page, Reorder, Resize, Sort} from "@syncfusion/ej2-react-grids";
import {WeatherForecast} from "../../ApiCient.ts";
interface WeatherGridProps {
/**
* Represents an optional array of weather forecast data.
* Each element in the array provides detailed weather forecast information.
*/
data?: WeatherForecast[];
testing?: boolean;
}
const NavMenu: React.FC<WeatherGridProps> = ({data: weatherData}) => {
return (
<div>
<GridComponent dataSource={weatherData || []} allowPaging={true} pageSettings={{pageCount: 5}} allowReordering={true} allowSorting={true}
allowFiltering={true} allowResizing={true}>
<ColumnsDirective>
<ColumnDirective field="date" format="dd.MM.yy" headerText="Date" width="150"/>
<ColumnDirective field="temperatureC" headerText="Temp. (C)" width="60"/>
<ColumnDirective field="temperatureF" headerText="Temp. (F)" width="60"/>
<ColumnDirective field="summary" headerText="Summary"/>
</ColumnsDirective>
<Inject services={[Page, Sort, Filter, Reorder, Resize]}/>
</GridComponent>
</div>
);
};
export default NavMenu;

View File

@@ -0,0 +1,75 @@
import with_main_layout from "../../layouts/with_main_layout.ts.tsx";
import useLocalStorageState from 'use-local-storage-state'; // install via npm
import {Client, WeatherForecast} from "../../ApiCient.ts";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faDownload, faTrash} from "@fortawesome/free-solid-svg-icons";
import {useState} from "react";
import WeatherGrid from "./WeatherGrid.tsx";
import {useSignalR} from "../../signalr/SignalRHelper.ts";
function WeatherPage() {
// Use the hook to persist your weather data. It will default to null.
const [weatherData, setWeatherData] = useLocalStorageState<WeatherForecast[] | null>('WeatherPage-Forecast', {defaultValue: [],});
const [loading, setLoading] = useState(false);
const [currentState, setCurrentState] = useState<string | null>(null);
const {subscribe} = useSignalR('http://localhost:5175', 'WeatherUpdateHub');
const fetchWeatherData = async () => {
const apiClient = new Client();
try {
const progressCleanup = subscribe<string>('ProgressUpdate', (_, message) => {
setCurrentState(message);
});
setLoading(true);
const data = await apiClient.getWeatherForecast();
setWeatherData(data);
setLoading(false);
progressCleanup();
setCurrentState(null);
} catch (error) {
console.error("Error fetching weather data:", error);
}
};
return (
<div className="card shadow-md card-sm w-full bg-auto-50">
<div className="card-header text-2xl ml-5 mt-1">Weather Data</div>
<div className="card-body">
<div className="row-auto flex gap-x-2 ">
<button className="btn w-48" onClick={fetchWeatherData}>
<FontAwesomeIcon className="mr-1" icon={faDownload}/>
Fetch Weather Data
</button>
<button className="btn w-48" onClick={() => setWeatherData(null)}>
<FontAwesomeIcon className="mr-1" icon={faTrash}/>
Clear Weather
</button>
</div>
<div className="mt-5">
{loading ?
<div>
{/*<p className="m-5">Progress: {currentState ? JSON.parse(currentState).percentage : 0}% - {currentState ? JSON.parse(currentState).message : ''}</p>*/}
{(() => {
const percentage = currentState ? JSON.parse(currentState).percentage : 0;
return (
<div className="radial-progress" style={{['--value' as unknown as string]: percentage}} aria-valuenow={percentage} role="progressbar">
{percentage}%
</div>
);
})()}
</div>
:
!weatherData || weatherData.length === 0 ? (
<p>No weather data, please request it!</p>
) : (
<WeatherGrid data={weatherData}/>
)}
</div>
</div>
</div>
);
}
export const PageWithLayout = with_main_layout(WeatherPage);
export default PageWithLayout;

View File

@@ -0,0 +1,63 @@
import {useCallback, useEffect, useRef, useState} from 'react';
import {HubConnection, HubConnectionBuilder, LogLevel} from '@microsoft/signalr';
export function useSignalR(baseUrl: string, hubName: string) {
const [connection, setConnection] = useState<HubConnection | null>(null);
const connectionRef = useRef<HubConnection | null>(null);
useEffect(() => {
const newConnection: HubConnection = new HubConnectionBuilder()
.withUrl(`${baseUrl}/${hubName}`)
.withAutomaticReconnect()
.configureLogging(LogLevel.Information)
.build();
connectionRef.current = newConnection;
setConnection(newConnection);
newConnection
.start()
.then(() => console.log('SignalR connected.'))
.catch(error => console.error('SignalR connection error:', error));
// Cleanup on unmount: stop the connection.
return () => {
newConnection.stop();
};
}, [baseUrl, hubName]);
/**
* Subscribes to a SignalR event.
* @param eventName The event name.
* @param callback The callback invoked when the event is received.
* @returns A function to unsubscribe the event.
*/
const subscribe = useCallback(
<T>(eventName: string, callback: (...args: T[]) => void): (() => void) => {
if (connectionRef.current) {
connectionRef.current.on(eventName, callback);
return () => connectionRef.current?.off(eventName, callback);
}
return () => {
};
},
[]
);
/**
* Sends a message via the SignalR connection.
* @param methodName The hub method name.
* @param args Arguments for the hub method.
*/
const sendMessage = useCallback(async (methodName: string, ...args: any[]): Promise<void> => {
if (connectionRef.current) {
try {
await connectionRef.current.send(methodName, ...args);
} catch (error) {
console.error('Error sending message:', error);
}
}
}, []);
return {connection, subscribe, sendMessage};
}

View File

@@ -0,0 +1,25 @@
export function handleDarkMode(): () => void {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
// Function to update dark mode for Syncfusion controls
const updateDarkMode = (e: MediaQueryList | MediaQueryListEvent) => {
if (e.matches) {
// If the system is in dark mode, add the class
document.body.classList.add("e-dark-mode");
} else {
// Otherwise, remove it
document.body.classList.remove("e-dark-mode");
}
};
// Set initial mode based on system preference:
updateDarkMode(mediaQuery); // Pass the initial `mediaQuery` directly
// Listen for changes in system preference:
mediaQuery.addEventListener("change", updateDarkMode);
// Cleanup function to remove the event listener
return () => {
mediaQuery.removeEventListener("change", updateDarkMode);
};
}

1
react-vite-client/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/layouts/mainlayout.tsx","./src/layouts/with_main_layout.ts.tsx","./src/theme_helpers/themehelpers.ts"],"errors":true,"version":"5.7.3"}

View File

@@ -0,0 +1,11 @@
import {defineConfig} from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})

1742
react-vite-client/yarn.lock Normal file

File diff suppressed because it is too large Load Diff