diff --git a/AdverCalculator.Server/AdverCalculator.Server.csproj b/AdverCalculator.Server/AdverCalculator.Server.csproj new file mode 100644 index 0000000..6728a9f --- /dev/null +++ b/AdverCalculator.Server/AdverCalculator.Server.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + true + ..\advercalculator.client + npm run dev + https://localhost:5173 + + + + + 8.*-* + + + + + + + false + + + + diff --git a/AdverCalculator.Server/AdverCalculator.Server.http b/AdverCalculator.Server/AdverCalculator.Server.http new file mode 100644 index 0000000..69af96d --- /dev/null +++ b/AdverCalculator.Server/AdverCalculator.Server.http @@ -0,0 +1,6 @@ +@AdverCalculator.Server_HostAddress = http://localhost:5129 + +GET {{AdverCalculator.Server_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/AdverCalculator.Server/Controllers/WeatherForecastController.cs b/AdverCalculator.Server/Controllers/WeatherForecastController.cs new file mode 100644 index 0000000..deaa98e --- /dev/null +++ b/AdverCalculator.Server/Controllers/WeatherForecastController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc; + +namespace AdverCalculator.Server.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } + } +} diff --git a/AdverCalculator.Server/Program.cs b/AdverCalculator.Server/Program.cs new file mode 100644 index 0000000..1da74a0 --- /dev/null +++ b/AdverCalculator.Server/Program.cs @@ -0,0 +1,30 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.UseDefaultFiles(); +app.UseStaticFiles(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.MapFallbackToFile("/index.html"); + +app.Run(); diff --git a/AdverCalculator.Server/Properties/launchSettings.json b/AdverCalculator.Server/Properties/launchSettings.json new file mode 100644 index 0000000..5fbf933 --- /dev/null +++ b/AdverCalculator.Server/Properties/launchSettings.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:40819", + "sslPort": 44354 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5129", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7102;http://localhost:5129", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + } + } + } +} + diff --git a/AdverCalculator.Server/WeatherForecast.cs b/AdverCalculator.Server/WeatherForecast.cs new file mode 100644 index 0000000..eb20e69 --- /dev/null +++ b/AdverCalculator.Server/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace AdverCalculator.Server +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/AdverCalculator.Server/appsettings.Development.json b/AdverCalculator.Server/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/AdverCalculator.Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/AdverCalculator.Server/appsettings.json b/AdverCalculator.Server/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/AdverCalculator.Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/AdverCalculator.sln b/AdverCalculator.sln new file mode 100644 index 0000000..15b5116 --- /dev/null +++ b/AdverCalculator.sln @@ -0,0 +1,33 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34525.116 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdverCalculator.Server", "AdverCalculator.Server\AdverCalculator.Server.csproj", "{A30B9DCF-BFFB-4ED5-8ECA-2A5B3622C3E9}" +EndProject +Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "advercalculator.client", "advercalculator.client\advercalculator.client.esproj", "{B1A52814-4EF1-454C-8D70-A98C51E6F102}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A30B9DCF-BFFB-4ED5-8ECA-2A5B3622C3E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A30B9DCF-BFFB-4ED5-8ECA-2A5B3622C3E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A30B9DCF-BFFB-4ED5-8ECA-2A5B3622C3E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A30B9DCF-BFFB-4ED5-8ECA-2A5B3622C3E9}.Release|Any CPU.Build.0 = Release|Any CPU + {B1A52814-4EF1-454C-8D70-A98C51E6F102}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1A52814-4EF1-454C-8D70-A98C51E6F102}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1A52814-4EF1-454C-8D70-A98C51E6F102}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {B1A52814-4EF1-454C-8D70-A98C51E6F102}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1A52814-4EF1-454C-8D70-A98C51E6F102}.Release|Any CPU.Build.0 = Release|Any CPU + {B1A52814-4EF1-454C-8D70-A98C51E6F102}.Release|Any CPU.Deploy.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {739EDA0F-95F0-4976-A013-130BFE32B767} + EndGlobalSection +EndGlobal diff --git a/advercalculator.client/.eslintrc.cjs b/advercalculator.client/.eslintrc.cjs new file mode 100644 index 0000000..d6c9537 --- /dev/null +++ b/advercalculator.client/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/advercalculator.client/.gitignore b/advercalculator.client/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/advercalculator.client/.gitignore @@ -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? diff --git a/advercalculator.client/README.md b/advercalculator.client/README.md new file mode 100644 index 0000000..0d6babe --- /dev/null +++ b/advercalculator.client/README.md @@ -0,0 +1,30 @@ +# 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 { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/advercalculator.client/advercalculator.client.esproj b/advercalculator.client/advercalculator.client.esproj new file mode 100644 index 0000000..54f5f9d --- /dev/null +++ b/advercalculator.client/advercalculator.client.esproj @@ -0,0 +1,11 @@ + + + npm run dev + src\ + Jest + + false + + $(MSBuildProjectDirectory)\dist + + \ No newline at end of file diff --git a/advercalculator.client/index.html b/advercalculator.client/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/advercalculator.client/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/advercalculator.client/nuget.config b/advercalculator.client/nuget.config new file mode 100644 index 0000000..6548586 --- /dev/null +++ b/advercalculator.client/nuget.config @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/advercalculator.client/package.json b/advercalculator.client/package.json new file mode 100644 index 0000000..17a396c --- /dev/null +++ b/advercalculator.client/package.json @@ -0,0 +1,28 @@ +{ + "name": "advercalculator.client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } +} diff --git a/advercalculator.client/public/vite.svg b/advercalculator.client/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/advercalculator.client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/advercalculator.client/src/App.css b/advercalculator.client/src/App.css new file mode 100644 index 0000000..0150819 --- /dev/null +++ b/advercalculator.client/src/App.css @@ -0,0 +1,19 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +tr:nth-child(even) { + background: #F2F2F2; +} + +tr:nth-child(odd) { + background: #FFF; +} + +th, td { + padding-left: 1rem; + padding-right: 1rem; +} \ No newline at end of file diff --git a/advercalculator.client/src/App.tsx b/advercalculator.client/src/App.tsx new file mode 100644 index 0000000..19c97a8 --- /dev/null +++ b/advercalculator.client/src/App.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react'; +import './App.css'; + +interface Forecast { + date: string; + temperatureC: number; + temperatureF: number; + summary: string; +} + +function App() { + const [forecasts, setForecasts] = useState(); + + useEffect(() => { + populateWeatherData(); + }, []); + + const contents = forecasts === undefined + ?

Loading... Please refresh once the ASP.NET backend has started. See https://aka.ms/jspsintegrationreact for more details.

+ : + + + + + + + + + + {forecasts.map(forecast => + + + + + + + )} + +
DateTemp. (C)Temp. (F)Summary
{forecast.date}{forecast.temperatureC}{forecast.temperatureF}{forecast.summary}
; + + return ( +
+

Weather forecast

+

This component demonstrates fetching data from the server.

+ {contents} +
+ ); + + async function populateWeatherData() { + const response = await fetch('weatherforecast'); + const data = await response.json(); + setForecasts(data); + } +} + +export default App; \ No newline at end of file diff --git a/advercalculator.client/src/assets/react.svg b/advercalculator.client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/advercalculator.client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/advercalculator.client/src/index.css b/advercalculator.client/src/index.css new file mode 100644 index 0000000..6119ad9 --- /dev/null +++ b/advercalculator.client/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/advercalculator.client/src/main.tsx b/advercalculator.client/src/main.tsx new file mode 100644 index 0000000..3d7150d --- /dev/null +++ b/advercalculator.client/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/advercalculator.client/src/vite-env.d.ts b/advercalculator.client/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/advercalculator.client/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/advercalculator.client/tsconfig.json b/advercalculator.client/tsconfig.json new file mode 100644 index 0000000..a7fc6fb --- /dev/null +++ b/advercalculator.client/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/advercalculator.client/tsconfig.node.json b/advercalculator.client/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/advercalculator.client/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/advercalculator.client/vite.config.ts b/advercalculator.client/vite.config.ts new file mode 100644 index 0000000..9a14d6f --- /dev/null +++ b/advercalculator.client/vite.config.ts @@ -0,0 +1,60 @@ +import { fileURLToPath, URL } from 'node:url'; + +import { defineConfig } from 'vite'; +import plugin from '@vitejs/plugin-react'; +import fs from 'fs'; +import path from 'path'; +import child_process from 'child_process'; + +const baseFolder = + process.env.APPDATA !== undefined && process.env.APPDATA !== '' + ? `${process.env.APPDATA}/ASP.NET/https` + : `${process.env.HOME}/.aspnet/https`; + +const certificateArg = process.argv.map(arg => arg.match(/--name=(?.+)/i)).filter(Boolean)[0]; +const certificateName = certificateArg ? certificateArg.groups.value : "advercalculator.client"; + +if (!certificateName) { + console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<> explicitly.') + process.exit(-1); +} + +const certFilePath = path.join(baseFolder, `${certificateName}.pem`); +const keyFilePath = path.join(baseFolder, `${certificateName}.key`); + +if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { + if (0 !== child_process.spawnSync('dotnet', [ + 'dev-certs', + 'https', + '--export-path', + certFilePath, + '--format', + 'Pem', + '--no-password', + ], { stdio: 'inherit', }).status) { + throw new Error("Could not create certificate."); + } +} + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [plugin()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + proxy: { + '^/weatherforecast': { + target: 'https://localhost:7102/', + secure: false + } + }, + port: 5173, + https: { + key: fs.readFileSync(keyFilePath), + cert: fs.readFileSync(certFilePath), + } + } +})