36 Commits

Author SHA1 Message Date
Ryan Peters
ca75f88cd8 fade in/out the toast 2025-07-17 17:15:21 -04:00
Ryan Peters
1279e16fad more style to the buttons and form 2025-07-17 17:13:27 -04:00
Ryan Peters
14e56285f5 wip making UI actions for notes 2025-07-17 16:42:25 -04:00
11f468a5c1 increase font size 2025-06-19 14:10:50 +00:00
d24065471b fix default note creation 2025-01-10 13:51:16 +00:00
Ryan Peters
538ddeeb4f adjusted save note argument signature 2025-01-09 11:41:18 -05:00
9a5a082f6d lighter color for links 2025-01-07 14:10:30 +00:00
Ryan Peters
bf9e8c0c69 adjust styling on password login and button 2025-01-06 08:59:47 -05:00
Ryan Peters
f35c2e499c modify logging message 2024-12-16 16:24:27 -05:00
Ryan Peters
769eabd9e4 try catch and logging in hub 2024-12-16 16:10:17 -05:00
Ryan Peters
d21a0e176a add some logging to the hub 2024-12-16 15:36:47 -05:00
Ryan Peters
f69d19100d remove old deploy script 2024-12-16 15:36:39 -05:00
Ryan Peters
dabd5d6a50 Merge branch 'master' of https://git.binarydad.com/ryan/Notes 2024-12-16 15:22:05 -05:00
Ryan Peters
1efa4b04d0 add login button 2024-12-16 15:22:03 -05:00
Ryan Peters
255b62a3b3 dark by default 2024-12-16 15:05:25 -05:00
e5cb210df2 method renames 2024-05-07 21:43:09 -04:00
aa3accb412 use alpine image 2024-05-08 01:34:16 +00:00
Ryan Peters
e20e8d907f reload page is WS connection failure 2024-01-25 20:12:50 -05:00
Ryan Peters
fe56bd903d make non nullable 2024-01-25 20:11:39 -05:00
6563838738 Merge branch 'master' of https://git.binarydad.com/ryan/Notes 2024-01-15 21:24:32 -05:00
6f5413ce95 exclude notes folder in git 2024-01-15 21:24:29 -05:00
Ryan Peters
9309b7012e make a notes folder in dockerfile 2024-01-08 17:17:38 -05:00
Ryan Peters
c38e4cd8bf fix merge issue with scripts 2024-01-06 21:15:50 -05:00
Ryan Peters
b5a4ce68a5 Merge branch 'dev/multiple-notes' 2024-01-06 21:10:06 -05:00
Ryan Peters
66111b81fe change framework for publish 2024-01-03 13:13:15 -05:00
Ryan Peters
be95f53647 change default port from 80 to 8080 as default 2023-11-15 11:25:16 -05:00
Ryan Peters
3eaa5c1910 update dockerfile for .net 8 2023-11-15 11:07:03 -05:00
Ryan Peters
ca8344cbb2 Merge branch 'master' of https://git.binarydad.com/ryan/Notes 2023-11-15 11:03:31 -05:00
Ryan Peters
3dbc7050de updated to .net 8 2023-11-15 11:03:26 -05:00
c307de995d add docker.sh 2023-11-03 12:23:18 +00:00
Ryan Peters
2acec128e8 add authorize to notehub 2023-10-30 10:07:35 -04:00
Ryan Peters
ca48d79f55 move scripts to appropriate pages 2023-10-29 16:15:47 -04:00
3a91b6db3c add env check for config 2023-08-29 20:26:04 -04:00
38287093b4 use iconfiguration for app passphrase 2023-08-29 20:25:45 -04:00
Ryan Peters
2e4903481c use expression bodies 2023-07-28 14:52:53 -04:00
Ryan Peters
70b0d0cba3 force sliding expiration to be true 2023-07-28 14:46:06 -04:00
17 changed files with 254 additions and 83 deletions

12
.config/dotnet-tools.json Normal file
View File

@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.0",
"commands": [
"dotnet-ef"
]
}
}
}

3
.gitignore vendored
View File

@@ -229,6 +229,9 @@ _pkginfo.txt
# but keep track of directories ending in .cache # but keep track of directories ending in .cache
!?*.[Cc]ache/ !?*.[Cc]ache/
# exclude notes
notes/*
# Others # Others
ClientBin/ ClientBin/
~$* ~$*

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>

View File

@@ -8,12 +8,9 @@ namespace BinaryDad.Notes.Controllers
{ {
private readonly INoteService noteService; private readonly INoteService noteService;
public ApiController(INoteService noteService) public ApiController(INoteService noteService) => this.noteService = noteService;
{
this.noteService = noteService;
}
[Route("note/{noteName}")] [Route("note/{noteName}")]
public string Note(string noteName) => noteService.GetText(noteName); public string Note(string noteName) => noteService.GetNote(noteName);
} }
} }

View File

@@ -8,6 +8,13 @@ namespace BinaryDad.Notes.Controllers
{ {
public class LoginController : Controller public class LoginController : Controller
{ {
private readonly IConfiguration configuration;
public LoginController(IConfiguration configuration)
{
this.configuration = configuration;
}
[Route("login")] [Route("login")]
public IActionResult Login() public IActionResult Login()
{ {
@@ -21,7 +28,7 @@ namespace BinaryDad.Notes.Controllers
{ {
if (ModelState.IsValid) if (ModelState.IsValid)
{ {
var appPassphrase = Environment.GetEnvironmentVariable("APP_PASSPHRASE"); var appPassphrase = configuration["APP_PASSPHRASE"];
if (passphrase == appPassphrase) if (passphrase == appPassphrase)
{ {

View File

@@ -10,10 +10,7 @@ public class NoteController : Controller
{ {
private readonly INoteService noteService; private readonly INoteService noteService;
public NoteController(INoteService noteService) public NoteController(INoteService noteService) => this.noteService = noteService;
{
this.noteService = noteService;
}
[Route("{noteName=default}")] [Route("{noteName=default}")]
public IActionResult Index(string noteName) public IActionResult Index(string noteName)
@@ -21,14 +18,22 @@ public class NoteController : Controller
var model = new ContentModel var model = new ContentModel
{ {
CurrentNote = noteName, CurrentNote = noteName,
Text = noteService.GetText(noteName), Text = noteService.GetNote(noteName),
NoteNames = noteService.GetNoteNames() NoteNames = noteService.GetNoteNames()
}; };
return View(model); return View(model);
} }
[Route("{noteName}/delete")] [HttpPost, Route("create")]
public IActionResult Create(string noteName)
{
noteService.SaveNote(noteName);
return Redirect($"/{noteName}");
}
[Route("delete/{noteName}")]
public IActionResult Delete(string noteName) public IActionResult Delete(string noteName)
{ {
noteService.DeleteNote(noteName); noteService.DeleteNote(noteName);

View File

@@ -1,13 +1,14 @@
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src WORKDIR /src
COPY . . COPY . .
RUN dotnet publish "BinaryDad.Notes.csproj" -c Release -o /app/publish /p:UseAppHost=false RUN dotnet publish "BinaryDad.Notes.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS base
WORKDIR /app WORKDIR /app
RUN mkdir notes
COPY --from=build /app/publish . COPY --from=build /app/publish .
EXPOSE 80 EXPOSE 8080
ENTRYPOINT ["dotnet", "BinaryDad.Notes.dll"] ENTRYPOINT ["dotnet", "BinaryDad.Notes.dll"]

View File

@@ -1,15 +1,19 @@
using BinaryDad.Notes.Services; using BinaryDad.Notes.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
namespace BinaryDad.Notes namespace BinaryDad.Notes
{ {
[Authorize]
public class NoteHub : Hub public class NoteHub : Hub
{ {
private readonly INoteService noteService; private readonly INoteService noteService;
private readonly ILogger<NoteHub> logger;
public NoteHub(INoteService noteService) public NoteHub(INoteService noteService, ILogger<NoteHub> logger)
{ {
this.noteService = noteService; this.noteService = noteService;
this.logger = logger;
} }
public override Task OnConnectedAsync() public override Task OnConnectedAsync()
@@ -21,20 +25,29 @@ namespace BinaryDad.Notes
return base.OnConnectedAsync(); return base.OnConnectedAsync();
} }
public async Task SaveNote(string content, string? noteName) public async Task SaveNote(string noteName, string content)
{ {
noteService.SaveText(content, noteName); try
{
noteService.SaveNote(noteName, content);
// find all other connections except for the current one // find all other connections except for the current one
var clientConnections = NoteContext.ClientNotes var clientConnections = NoteContext.ClientNotes
.Where(c => c.Value == noteName && c.Key != Context.ConnectionId) .Where(c => c.Value == noteName && c.Key != Context.ConnectionId)
.Select(c => c.Key) .Select(c => c.Key)
.ToList(); .ToList();
// update note for all other clients // update note for all other clients
await Clients await Clients
.Clients(clientConnections) .Clients(clientConnections)
.SendAsync("updateNote", content); .SendAsync("updateNote", content);
logger.LogInformation($"Note \"{noteName}\" saved! Updated {clientConnections.Count} other client(s).");
}
catch (Exception ex)
{
logger.LogError($"Unable to save note \"{noteName}\" => {ex}");
}
} }
} }
} }

View File

@@ -4,6 +4,8 @@ using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddEnvironmentVariables();
// Add services to the container. // Add services to the container.
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
builder.Services.AddSignalR(); builder.Services.AddSignalR();
@@ -14,6 +16,7 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
o.LoginPath = "/login"; o.LoginPath = "/login";
o.Cookie.Name = "NotesUser"; o.Cookie.Name = "NotesUser";
o.Cookie.MaxAge = TimeSpan.FromDays(3); o.Cookie.MaxAge = TimeSpan.FromDays(3);
o.SlidingExpiration = true;
}); });
var app = builder.Build(); var app = builder.Build();

View File

@@ -14,7 +14,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<WebPublishMethod>FileSystem</WebPublishMethod> <WebPublishMethod>FileSystem</WebPublishMethod>
<_TargetId>Folder</_TargetId> <_TargetId>Folder</_TargetId>
<SiteUrlToLaunchAfterPublish /> <SiteUrlToLaunchAfterPublish />
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<ProjectGuid>bf137709-fcd2-4bb0-ade0-8fc71a244485</ProjectGuid> <ProjectGuid>bf137709-fcd2-4bb0-ade0-8fc71a244485</ProjectGuid>
<SelfContained>false</SelfContained> <SelfContained>false</SelfContained>

View File

@@ -16,7 +16,7 @@
} }
} }
public string GetText(string noteName) public string GetNote(string noteName)
{ {
CheckFile(noteName); CheckFile(noteName);
@@ -30,8 +30,13 @@
.ToList(); .ToList();
} }
public void SaveText(string content, string noteName) public void SaveNote(string noteName, string? content = null)
{ {
if (string.IsNullOrWhiteSpace(noteName))
{
content = "Hi! Feel free to start typing. Everything will be saved soon after you are done typing.";
}
File.WriteAllText(GetFilePath(noteName), content); File.WriteAllText(GetFilePath(noteName), content);
} }
@@ -51,7 +56,7 @@
{ {
Directory.CreateDirectory(folderPath); Directory.CreateDirectory(folderPath);
SaveText("Hi! Feel free to start typing. Everything will be saved soon after you are done typing.", noteName); SaveNote(noteName);
} }
} }

View File

@@ -3,8 +3,8 @@
public interface INoteService public interface INoteService
{ {
ICollection<string> GetNoteNames(); ICollection<string> GetNoteNames();
string GetText(string noteName); string GetNote(string noteName);
void SaveText(string content, string noteName); void SaveNote(string noteName, string? content = null);
void DeleteNote(string noteName); void DeleteNote(string noteName);
} }
} }

View File

@@ -1,7 +1,8 @@
@Html.ValidationSummary() @Html.ValidationSummary()
<form method="post"> <form method="post" class="login-form">
<input type="password" name="passphrase" placeholder="Passphrase" /> <input type="password" name="passphrase" placeholder="Passphrase" />
<button type="submit">Login</button>
</form> </form>
@section scripts { @section scripts {

View File

@@ -2,13 +2,20 @@
<textarea id="content" name="content" spellcheck="false">@Model.Text</textarea> <textarea id="content" name="content" spellcheck="false">@Model.Text</textarea>
<div class="note-names"> <div class="note-actions">
@foreach (var note in Model.NoteNames.Order()) <select id="note-dropdown" class="note-dropdown">
{ @foreach (var note in Model.NoteNames.Order())
var css = note.Equals(Model.CurrentNote, StringComparison.OrdinalIgnoreCase) ? "current" : null; {
var selected = note.Equals(Model.CurrentNote, StringComparison.OrdinalIgnoreCase) ? "selected" : null;
<option value="@note" selected="@selected">@note</option>
}
</select>
<a href="@note" class="@css">@note</a> <a asp-action="Delete" asp-controller="Note" asp-route-noteName="@Model.CurrentNote" class="btn-symbol btn-delete" title="Delete Note">✕</a>
} <form method="post" class="action-form" asp-action="Create" asp-controller="Note">
<input type="text" name="noteName" placeholder="Note name" />
<button type="submit" class="btn-symbol btn-create" title="Create Note">💾</button>
</form>
</div> </div>
<div class="toast" id="saved-indicator">Saved</div> <div class="toast" id="saved-indicator">Saved</div>
@@ -19,4 +26,7 @@
<script> <script>
var noteName = '@Model.CurrentNote'; var noteName = '@Model.CurrentNote';
</script> </script>
<script src="~/lib/signalr/dist/browser/signalr.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
} }

View File

@@ -8,14 +8,12 @@
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head> </head>
<body> <body class="dark">
@RenderBody() @RenderBody()
@RenderSection("scripts", false)
<script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/signalr/dist/browser/signalr.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script> @RenderSection("scripts", false)
</body> </body>

View File

@@ -20,27 +20,126 @@ body {
min-height: 100%; min-height: 100%;
} }
div.note-names { div.note-actions {
position: fixed; position: fixed;
bottom: 5px; bottom: 5px;
left: 0; left: 0;
font-size: 14px; font-size: 14px;
opacity: 0.5; display: flex;
align-items: center;
gap: 8px;
padding: 8px;
} }
div.note-names a { select.note-dropdown {
color: #666; padding: 6px 8px;
padding-left: 10px; border-radius: 5px;
text-decoration: none; border: 1px solid #ccc;
background-color: #fff;
font-size: 13px;
min-width: 120px;
max-width: 150px;
}
body.dark select.note-dropdown {
background-color: #333;
color: #ddd;
border: 1px solid #555;
}
form.action-form {
display: inline-flex;
align-items: center;
gap: 4px;
}
form.action-form input[type="text"] {
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 13px;
width: 100px;
background-color: #fff;
}
body.dark form.action-form input[type="text"] {
background-color: #333;
color: #ddd;
border: 1px solid #555;
}
form.action-form input[type="text"]:focus {
outline: none;
border-color: #009E60;
box-shadow: 0 0 0 2px rgba(0, 158, 96, 0.2);
}
.btn-symbol {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 5px;
border: 1px solid;
text-decoration: none;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
line-height: 1;
}
.btn-delete {
background-color: #dc3545;
color: #fff;
border-color: #dc3545;
}
.btn-delete:hover {
background-color: #c82333;
border-color: #bd2130;
transform: scale(1.05);
}
.btn-create {
background-color: #009E60;
color: #fff;
border-color: #009E60;
}
.btn-create:hover {
background-color: #007a4d;
border-color: #006b42;
transform: scale(1.05);
}
/* Mobile responsiveness */
@media (max-width: 768px) {
div.note-actions {
gap: 6px;
padding: 6px;
font-size: 12px;
} }
div.note-names a.current { select.note-dropdown {
font-weight: bold; min-width: 100px;
} max-width: 120px;
font-size: 12px;
div .note-names a:not(:last-of-type) { padding: 5px 6px;
border-right: 1px solid #666; }
}
form.action-form input[type="text"] {
width: 80px;
font-size: 12px;
padding: 5px 6px;
}
.btn-symbol {
width: 24px;
height: 24px;
font-size: 12px;
}
}
textarea { textarea {
width: 100%; width: 100%;
@@ -49,7 +148,7 @@ textarea {
margin: 0; margin: 0;
resize: none; resize: none;
color: #444; color: #444;
font-size: 12px; font-size: 16px;
font-family: Consolas, 'Courier New', monospace; font-family: Consolas, 'Courier New', monospace;
outline: none; outline: none;
position: absolute; position: absolute;
@@ -57,7 +156,9 @@ textarea {
border-width: 0; border-width: 0;
} }
body.dark, body.dark input, body.dark textarea { body.dark,
body.dark input,
body.dark textarea {
background-color: #222; background-color: #222;
color: #ddd; color: #ddd;
} }
@@ -69,29 +170,41 @@ body.dark, body.dark input, body.dark textarea {
font-size: 11px; font-size: 11px;
padding: 6px; padding: 6px;
color: #fff; color: #fff;
transition: bottom 0.3s; transition: bottom 0.3s, opacity 0.3s;
opacity: 0.8; opacity: 0;
border-radius: 5px 5px 0 0; border-radius: 5px 5px 0 0;
} }
.toast.show { .toast.show {
bottom: 0; bottom: 0;
} opacity: 0.8;
}
.toast#saved-indicator { .toast#saved-indicator {
background-color: green; background-color: green;
} }
.toast#update-indicator { .toast#update-indicator {
background-color: orangered; background-color: orangered;
} }
form input[type=password] { form.login-form input[type=password],
form.login-form button {
display: block; display: block;
width: 100%;
max-width: 300px;
margin: 20px auto; margin: 20px auto;
font-size: 20px; font-size: 20px;
padding: 8px; padding: 8px;
border: 1px solid #999; border: 1px solid #333;
border-radius: 4px; border-radius: 8px;
box-sizing: border-box;
}
form.login-form input[type=password] {
color: #999; color: #999;
} }
button, select {
cursor: pointer;
}

View File

@@ -9,7 +9,7 @@ function start() {
console.log('Started websocket listener'); console.log('Started websocket listener');
}).catch(function (err) { }).catch(function (err) {
console.error(err.toString()); console.error(err.toString());
return alert('Connection error. Reload page.'); location.reload();
}); });
} }
@@ -29,7 +29,7 @@ function saveContent($textarea) {
var content = $textarea.val(); var content = $textarea.val();
connection.invoke('SaveNote', content, noteName).then(function () { connection.invoke('SaveNote', noteName, content).then(function () {
showToast('#saved-indicator'); showToast('#saved-indicator');
}).catch(function (err) { }).catch(function (err) {
console.error(err.toString()); console.error(err.toString());
@@ -47,6 +47,14 @@ $(function () {
// set focus on load // set focus on load
$textarea.focus(); $textarea.focus();
// handle note dropdown change
$('#note-dropdown').change(function () {
var selectedNote = $(this).val();
if (selectedNote && selectedNote !== noteName) {
window.location.href = selectedNote;
}
});
// update content upon sync save // update content upon sync save
connection.on('updateNote', function (content) { connection.on('updateNote', function (content) {
$textarea.val(content); $textarea.val(content);
@@ -63,11 +71,6 @@ $(function () {
}); });
}); });
// set dark mode
if (window.location.hash == '#dark') {
$('body').addClass('dark');
}
let timer = null; let timer = null;
const ignoredKeyCodes = [17, 18, 20, 27, 37, 38, 39, 40, 91]; const ignoredKeyCodes = [17, 18, 20, 27, 37, 38, 39, 40, 91];