Compare commits

..

34 commits
v0.1.0 ... main

Author SHA1 Message Date
92715e9a97
chore: bump to 0.2.1
All checks were successful
Checks / check (push) Successful in -6m19s
Build and Push Docker Image / Build and Push Image (push) Successful in -5m23s
2024-12-23 19:10:23 +02:00
3deeafd084
style(calendar): make the size persistent
All checks were successful
Checks / check (push) Successful in -6m13s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m9s
2024-12-23 19:02:18 +02:00
9a41ca7e98
style: mix format
All checks were successful
Checks / check (push) Successful in -6m22s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m16s
2024-12-23 00:13:58 +02:00
4ef23a5ec9
fix: dark mode for calendar
Some checks failed
Build and Push Docker Image / Build and Push Image (push) Waiting to run
Checks / check (push) Has been cancelled
2024-12-23 00:13:29 +02:00
a724826a59
style: mix format 2024-12-23 00:10:10 +02:00
c60a5e0f12
fix: wrong day of the week
Some checks failed
Checks / check (push) Failing after -6m21s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m4s
2024-12-22 22:53:56 +02:00
223939da6f
feat: calendar view
Some checks failed
Checks / check (push) Failing after -6m20s
Build and Push Docker Image / Build and Push Image (push) Has been cancelled
2024-12-22 22:52:30 +02:00
d5f7ddf250
chore: update README
All checks were successful
Checks / check (push) Successful in -5m35s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m50s
2024-12-22 22:18:23 +02:00
f18fc0ab63
chore: bump to 0.2.0
All checks were successful
Checks / check (push) Successful in -4m26s
Build and Push Docker Image / Build and Push Image (push) Successful in -3m52s
i finally remembered that with each feat commit you're supposed to bump
the minor, not the patch. so i bumped to 0.2.0
2024-12-14 00:35:26 +02:00
41c67fe35e
style: remove "Phoenix Framework" from tab title 2024-12-14 00:34:04 +02:00
ff9a3a8e92
trans: Translate Register and Log in
kinda forgot about those
2024-12-14 00:33:01 +02:00
053bfed5e6
chore: bump to 0.1.4
All checks were successful
Checks / check (push) Successful in -6m23s
Build and Push Docker Image / Build and Push Image (push) Successful in -5m25s
2024-12-12 18:59:36 +02:00
b5d52426b6 Merge pull request 'feat: add i18n' (#3) from i18n into main
All checks were successful
Checks / check (push) Successful in -6m24s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m1s
Reviewed-on: #3
2024-12-12 18:56:58 +02:00
65308f8bda
style: mix format
All checks were successful
Checks / check (pull_request) Successful in -6m26s
2024-12-12 18:52:36 +02:00
f7f0ebbb25
trans: errors.po 2024-12-12 18:52:07 +02:00
7380d3da3c
trans: switch to ngettext for "n days left"
Some checks failed
Checks / check (pull_request) Failing after -6m14s
2024-12-12 18:19:53 +02:00
a5df93e4b7 Merge branch 'main' into i18n
Some checks failed
Checks / check (pull_request) Failing after -6m27s
2024-12-11 22:38:13 +02:00
bb455f793a
chore: update phoenix_live_view to 1.0.0
Some checks failed
Checks / check (push) Failing after -6m27s
Build and Push Docker Image / Build and Push Image (push) Successful in -5m27s
2024-12-11 22:36:47 +02:00
094b0f0324
style: mix format
All checks were successful
Checks / check (pull_request) Successful in -6m20s
2024-12-11 22:22:40 +02:00
9e9d8ffdaf
trans: settings and new exam modal
Some checks failed
Checks / check (pull_request) Failing after -6m8s
translated those to russian
2024-12-11 22:18:34 +02:00
cf28aa5295
feat: add i18n
Some checks failed
Checks / check (pull_request) Failing after -6m26s
right not, the most of the app is translated, but there are some parts that are not translated yet, like the settings page.
2024-12-10 00:39:52 +02:00
c254254f57
style: fix formatting
All checks were successful
Checks / check (push) Successful in -6m17s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m15s
2024-12-09 21:05:13 +02:00
9893b278e2
chore: bump to 0.1.3
Some checks failed
Checks / check (push) Failing after -5m57s
Build and Push Docker Image / Build and Push Image (push) Has been cancelled
2024-12-09 21:02:49 +02:00
c7b4373aef
feat: show day delta 2024-12-09 21:01:47 +02:00
13a1f32d8d
ci: migrate to forgejo
All checks were successful
Checks / check (push) Successful in -6m19s
Build and Push Docker Image / Build and Push Image (push) Successful in -6m56s
2024-12-07 16:27:41 +02:00
08108ef71a
fix: dark mode on edit and add
All checks were successful
Checks / check (push) Successful in -2m56s
Build and Push Docker Image / Build and Push Image (push) Successful in -2m49s
2024-12-02 17:31:03 +02:00
bc77739425
chore: bump version to 0.1.2
All checks were successful
Checks / check (push) Successful in -2m56s
Build and Push Docker Image / Build and Push Image (push) Successful in -1m54s
2024-12-01 19:28:37 +02:00
eee0b548e9
Merge pull request 'feat: add dark mode' (#2) from dark-mode into main
All checks were successful
Checks / check (push) Successful in -2m55s
Build and Push Docker Image / Build and Push Image (push) Successful in -3m25s
Reviewed-on: #2
2024-12-01 19:20:45 +02:00
6f47a56653
feat: add dark mode
All checks were successful
Checks / check (pull_request) Successful in -2m56s
2024-12-01 18:48:39 +02:00
b98cb3e145
chore: add license
All checks were successful
Checks / check (push) Successful in -2m24s
Build and Push Docker Image / Build and Push Image (push) Successful in -3m19s
2024-12-01 17:10:36 +02:00
91c4ca2bc2
style: fix casing
All checks were successful
Checks / check (push) Successful in -2m51s
Build and Push Docker Image / Build and Push Image (push) Successful in -3m32s
2024-12-01 00:44:29 +02:00
c136d7eb07
ci: add tags to release workflow
All checks were successful
Checks / check (push) Successful in 1m30s
Build and Push Docker Image / Build and Push Image (push) Successful in 35s
2024-11-30 22:49:44 +02:00
cb4aba5860
chore: bump version to 0.1.1
All checks were successful
Checks / check (push) Successful in 1m31s
Build and Push Docker Image / Build and Push Image (push) Successful in 2m25s
2024-11-30 22:37:06 +02:00
4ec7fc5cf3
ci: add release workflow
All checks were successful
Checks / check (push) Successful in 1m35s
Build and Push Docker Image / Build and Push Image (push) Successful in 33s
2024-11-30 20:39:06 +02:00
33 changed files with 1411 additions and 200 deletions

View file

@ -19,7 +19,7 @@ jobs:
- name: Setup ssl
run: wget http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2.23_amd64.deb && sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2.23_amd64.deb
- uses: erlef/setup-beam@v1
- uses: https://github.com/erlef/setup-beam@v1
env:
ImageOS: ubuntu24
with:

View file

@ -3,12 +3,12 @@ name: Build and Push Docker Image
on:
push:
branches:
- main # Change this to your default branch if different
- main
jobs:
build:
name: Build and Push Image
runs-on: ubuntu-latest-root # This specifies the environment for the job
runs-on: ubuntu-latest-root
steps:
- name: Checkout Repository
@ -17,9 +17,9 @@ jobs:
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: git.vavakado.xyz # Replace with your Docker registry URL
username: ${{ gitea.actor }} # The actor who triggered the action
password: ${{ secrets.REGISTRY_TOKEN }} # Your registry token secret
registry: git.vavakado.xyz
username: vavakado
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build Docker Image
run: |

View file

@ -0,0 +1,67 @@
name: Manual Release and Docker Image Build
on:
workflow_dispatch:
inputs:
version:
description: "Version for the release (e.g., v1.0.0)"
required: true
default: "v1.0.0"
type: string
jobs:
release:
name: Create Release and Build Docker Image
runs-on: ubuntu-latest-root
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: git.vavakado.xyz
username: vavakado
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Create Git Tag
run: |
git tag ${{ github.event.inputs.version }}
git push origin ${{ github.event.inputs.version }}
# - name: Create Release
# run: |
# curl -X POST -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
# -H "Content-Type: application/json" \
# -d '{
# "tag_name": "${{ github.event.inputs.version }}",
# "target_commitish": "main", # Change if your default branch is different
# "name": "${{ github.event.inputs.version }}",
# "body": "Release for version ${{ github.event.inputs.version }}",
# "draft": false,
# "prerelease": false
# }' \
# https://git.vavakado.xyz/api/v1/repos/${{ gitea.repository_owner }}/${{ gitea.repository }}/releases
# - name: Create Release
# uses: actions/forgejo-release@v2
# with:
# direction: upload
# tag: ${{ github.event.inputs.version }}
# title: ${{ github.event.inputs.version }}
# token: ${{ secrets.RELEASE_TOKEN }}
# release-notes-assistant: true
# release-dir: ./
- name: Build Docker Image
run: |
docker build -t git.vavakado.xyz/${{ gitea.repository_owner }}/exmr:${{ github.event.inputs.version }} .
docker tag git.vavakado.xyz/vavakado/exmr:${{ github.event.inputs.version }} git.vavakado.xyz/vavakado/exmr:latest
docker tag git.vavakado.xyz/vavakado/exmr:${{ github.event.inputs.version }} git.vavakado.xyz/vavakado/exmr:stable
- name: Push Docker Image
run: |
docker push git.vavakado.xyz/${{ gitea.repository_owner }}/exmr:${{ github.event.inputs.version }}
docker push git.vavakado.xyz/vavakado/exmr:latest
docker push git.vavakado.xyz/vavakado/exmr:stable

View file

@ -1,52 +0,0 @@
name: Manual Release and Docker Image Build
on:
workflow_dispatch:
inputs:
version:
description: "Version for the release (e.g., v1.0.0)"
required: true
default: "v1.0.0"
jobs:
release:
name: Create Release and Build Docker Image
runs-on: ubuntu-latest-root
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: git.vavakado.xyz # Replace with your Docker registry URL
username: ${{ gitea.actor }} # The actor who triggered the action
password: ${{ secrets.REGISTRY_TOKEN }} # Your registry token secret
- name: Create Git Tag
run: |
git tag ${{ github.event.inputs.version }}
git push origin ${{ github.event.inputs.version }}
- name: Create Release
run: |
curl -X POST -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"tag_name": "${{ github.event.inputs.version }}",
"target_commitish": "main", # Change if your default branch is different
"name": "${{ github.event.inputs.version }}",
"body": "Release for version ${{ github.event.inputs.version }}",
"draft": false,
"prerelease": false
}' \
https://git.vavakado.xyz/api/v1/repos/${{ gitea.repository_owner }}/${{ gitea.repository }}/releases
- name: Build Docker Image
run: |
docker build -t git.vavakado.xyz/${{ gitea.repository_owner }}/your-image-name:${{ github.event.inputs.version }} .
- name: Push Docker Image
run: |
docker push git.vavakado.xyz/${{ gitea.repository_owner }}/your-image-name:${{ github.event.inputs.version }}

View file

@ -18,7 +18,7 @@ ARG DEBIAN_VERSION=bullseye-20241111-slim
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
FROM ${BUILDER_IMAGE} as builder
FROM ${BUILDER_IMAGE} AS builder
# install build dependencies
RUN apt-get update -y && apt-get install -y build-essential git \

8
LICENSE Normal file
View file

@ -0,0 +1,8 @@
Copyright 2024 Vladimir Rubin
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -9,12 +9,8 @@ To start your Phoenix server:
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
## TODO
## Learn more
- Official website: https://www.phoenixframework.org/
- Guides: https://hexdocs.pm/phoenix/overview.html
- Docs: https://hexdocs.pm/phoenix
- Forum: https://elixirforum.com/c/phoenix-forum
- Source: https://github.com/phoenixframework/phoenix
- [ ] Add tests
- [ ] Add a proper readme
- [ ] Add a calendar view

View file

@ -16,29 +16,35 @@
//
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
import "phoenix_html";
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
import { Socket } from "phoenix";
import { LiveSocket } from "phoenix_live_view";
import topbar from "../vendor/topbar";
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
import darkModeHook from "../vendor/dark_mode";
let Hooks = {};
Hooks.DarkThemeToggle = darkModeHook;
let csrfToken = document
.querySelector("meta[name='csrf-token']")
.getAttribute("content");
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken}
})
longPollFallbackMs: 2500,
params: { _csrf_token: csrfToken },
hooks: Hooks,
});
// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" });
window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300));
window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide());
// connect if there are any LiveViews on the page
liveSocket.connect()
liveSocket.connect();
// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket
window.liveSocket = liveSocket;

View file

@ -7,6 +7,7 @@ const path = require("path");
module.exports = {
content: ["./js/**/*.js", "../lib/exmr_web.ex", "../lib/exmr_web/**/*.*ex"],
darkMode: "class",
theme: {
extend: {
colors: {

46
assets/vendor/dark_mode.js vendored Normal file
View file

@ -0,0 +1,46 @@
const localStorageKey = "theme";
const isDark = () => {
if (localStorage.getItem(localStorageKey) === "dark") return true;
if (localStorage.getItem(localStorageKey) === "light") return false;
return window.matchMedia("(prefers-color-scheme: dark)").matches;
};
const setupThemeToggle = () => {
toggleVisibility = (dark) => {
const themeToggleDarkIcon = document.getElementById(
"theme-toggle-dark-icon",
);
const themeToggleLightIcon = document.getElementById(
"theme-toggle-light-icon",
);
if (themeToggleDarkIcon == null || themeToggleLightIcon == null) return;
const show = dark ? themeToggleDarkIcon : themeToggleLightIcon;
const hide = dark ? themeToggleLightIcon : themeToggleDarkIcon;
show.classList.remove("hidden", "text-transparent");
hide.classList.add("hidden", "text-transparent");
if (dark) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
try {
localStorage.setItem(localStorageKey, dark ? "dark" : "light");
} catch (_err) { }
};
toggleVisibility(isDark());
document
.getElementById("theme-toggle")
.addEventListener("click", function() {
toggleVisibility(!isDark());
});
};
const darkModeHook = {
mounted() {
setupThemeToggle();
},
updated() { },
};
export default darkModeHook;

View file

@ -48,9 +48,13 @@ defmodule ExmrWeb.CoreComponents do
phx-mounted={@show && show_modal(@id)}
phx-remove={hide_modal(@id)}
data-cancel={JS.exec(@on_cancel, "phx-remove")}
class="relative z-50 hidden"
class="relative z-50 hidden bg-zinc-50/90 dark:bg-zinc-950"
>
<div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" />
<div
id={"#{@id}-bg"}
class="bg-zinc-50/90 dark:bg-zinc-800/90 fixed inset-0 transition-opacity"
aria-hidden="true"
/>
<div
class="fixed inset-0 overflow-y-auto"
aria-labelledby={"#{@id}-title"}
@ -66,7 +70,7 @@ defmodule ExmrWeb.CoreComponents do
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
phx-key="escape"
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white dark:bg-zinc-950 p-14 shadow-lg ring-1 transition"
>
<div class="absolute top-6 right-5">
<button
@ -79,7 +83,7 @@ defmodule ExmrWeb.CoreComponents do
</button>
</div>
<div id={"#{@id}-content"}>
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</div>
</.focus_wrap>
</div>
@ -124,9 +128,9 @@ defmodule ExmrWeb.CoreComponents do
<p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
<.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
<.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
<%= @title %>
{@title}
</p>
<p class="mt-2 text-sm leading-5"><%= msg %></p>
<p class="mt-2 text-sm leading-5">{msg}</p>
<button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}>
<.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
</button>
@ -157,7 +161,7 @@ defmodule ExmrWeb.CoreComponents do
phx-connected={hide("#client-error")}
hidden
>
<%= gettext("Attempting to reconnect") %>
{gettext("Attempting to reconnect")}
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
</.flash>
@ -169,7 +173,7 @@ defmodule ExmrWeb.CoreComponents do
phx-connected={hide("#server-error")}
hidden
>
<%= gettext("Hang in there while we get back on track") %>
{gettext("Hang in there while we get back on track")}
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
</.flash>
</div>
@ -202,10 +206,13 @@ defmodule ExmrWeb.CoreComponents do
def simple_form(assigns) do
~H"""
<.form :let={f} for={@for} as={@as} {@rest}>
<div class="mt-10 space-y-8 bg-white">
<%= render_slot(@inner_block, f) %>
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
<%= render_slot(action, f) %>
<div class="mt-10 space-y-8 bg-white dark:bg-zinc-950">
{render_slot(@inner_block, f)}
<div
:for={action <- @actions}
class="mt-2 flex items-center justify-between gap-6 dark:text-zinc-100"
>
{render_slot(action, f)}
</div>
</div>
</.form>
@ -232,12 +239,12 @@ defmodule ExmrWeb.CoreComponents do
type={@type}
class={[
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
"text-sm font-semibold leading-6 text-white active:text-white/80",
"text-sm font-semibold leading-6",
@class
]}
{@rest}
>
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</button>
"""
end
@ -310,7 +317,7 @@ defmodule ExmrWeb.CoreComponents do
~H"""
<div>
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600 dark:text-zinc-200">
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
<input
type="checkbox"
@ -318,12 +325,12 @@ defmodule ExmrWeb.CoreComponents do
name={@name}
value="true"
checked={@checked}
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
{@rest}
/>
<%= @label %>
{@label}
</label>
<.error :for={msg <- @errors}><%= msg %></.error>
<.error :for={msg <- @errors}>{msg}</.error>
</div>
"""
end
@ -331,7 +338,7 @@ defmodule ExmrWeb.CoreComponents do
def input(%{type: "select"} = assigns) do
~H"""
<div>
<.label for={@id}><%= @label %></.label>
<.label for={@id}>{@label}</.label>
<select
id={@id}
name={@name}
@ -339,10 +346,10 @@ defmodule ExmrWeb.CoreComponents do
multiple={@multiple}
{@rest}
>
<option :if={@prompt} value=""><%= @prompt %></option>
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
<option :if={@prompt} value="">{@prompt}</option>
{Phoenix.HTML.Form.options_for_select(@options, @value)}
</select>
<.error :for={msg <- @errors}><%= msg %></.error>
<.error :for={msg <- @errors}>{msg}</.error>
</div>
"""
end
@ -350,7 +357,7 @@ defmodule ExmrWeb.CoreComponents do
def input(%{type: "textarea"} = assigns) do
~H"""
<div>
<.label for={@id}><%= @label %></.label>
<.label for={@id}>{@label}</.label>
<textarea
id={@id}
name={@name}
@ -361,7 +368,7 @@ defmodule ExmrWeb.CoreComponents do
]}
{@rest}
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
<.error :for={msg <- @errors}><%= msg %></.error>
<.error :for={msg <- @errors}>{msg}</.error>
</div>
"""
end
@ -370,7 +377,7 @@ defmodule ExmrWeb.CoreComponents do
def input(assigns) do
~H"""
<div>
<.label for={@id}><%= @label %></.label>
<.label for={@id}>{@label}</.label>
<input
type={@type}
name={@name}
@ -383,7 +390,7 @@ defmodule ExmrWeb.CoreComponents do
]}
{@rest}
/>
<.error :for={msg <- @errors}><%= msg %></.error>
<.error :for={msg <- @errors}>{msg}</.error>
</div>
"""
end
@ -396,8 +403,8 @@ defmodule ExmrWeb.CoreComponents do
def label(assigns) do
~H"""
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
<%= render_slot(@inner_block) %>
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800 dark:text-zinc-200">
{render_slot(@inner_block)}
</label>
"""
end
@ -411,7 +418,7 @@ defmodule ExmrWeb.CoreComponents do
~H"""
<p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600">
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</p>
"""
end
@ -429,14 +436,14 @@ defmodule ExmrWeb.CoreComponents do
~H"""
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
<div>
<h1 class="text-lg font-semibold leading-8 text-zinc-800">
<%= render_slot(@inner_block) %>
<h1 class="text-lg font-semibold leading-none text-zinc-800 dark:text-zinc-200">
{render_slot(@inner_block)}
</h1>
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
<%= render_slot(@subtitle) %>
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600 dark:text-zinc-100">
{render_slot(@subtitle)}
</p>
</div>
<div class="flex-none"><%= render_slot(@actions) %></div>
<div class="flex-none">{render_slot(@actions)}</div>
</header>
"""
end
@ -477,9 +484,9 @@ defmodule ExmrWeb.CoreComponents do
<table class="w-[40rem] mt-11 sm:w-full">
<thead class="text-sm text-left leading-6 text-zinc-500">
<tr>
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%= col[:label] %></th>
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal">{col[:label]}</th>
<th :if={@action != []} class="relative p-0 pb-4">
<span class="sr-only"><%= gettext("Actions") %></span>
<span class="sr-only">{gettext("Actions")}</span>
</th>
</tr>
</thead>
@ -497,7 +504,7 @@ defmodule ExmrWeb.CoreComponents do
<div class="block py-4 pr-6">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
<%= render_slot(col, @row_item.(row)) %>
{render_slot(col, @row_item.(row))}
</span>
</div>
</td>
@ -508,7 +515,7 @@ defmodule ExmrWeb.CoreComponents do
:for={action <- @action}
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
<%= render_slot(action, @row_item.(row)) %>
{render_slot(action, @row_item.(row))}
</span>
</div>
</td>
@ -538,8 +545,8 @@ defmodule ExmrWeb.CoreComponents do
<div class="mt-14">
<dl class="-my-4 divide-y divide-zinc-100">
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
<dt class="w-1/4 flex-none text-zinc-500"><%= item.title %></dt>
<dd class="text-zinc-700"><%= render_slot(item) %></dd>
<dt class="w-1/4 flex-none text-zinc-500">{item.title}</dt>
<dd class="text-zinc-700">{render_slot(item)}</dd>
</div>
</dl>
</div>
@ -564,7 +571,7 @@ defmodule ExmrWeb.CoreComponents do
class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
<.icon name="hero-arrow-left-solid" class="h-3 w-3" />
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</.link>
</div>
"""

View file

@ -0,0 +1,57 @@
defmodule DarkMode do
@moduledoc """
A component to toggle dark mode.
"""
use Phoenix.Component
def button(assigns) do
~H"""
<button
id="theme-toggle"
type="button"
phx-update="ignore"
phx-hook="DarkThemeToggle"
class="text-zinc-500 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-700 rounded-lg text-sm p-2"
>
<svg
id="theme-toggle-dark-icon"
class="w-5 h-5 text-transparent hidden"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
<svg
id="theme-toggle-light-icon"
class="w-5 h-5 text-transparent"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
fill-rule="evenodd"
clip-rule="evenodd"
>
</path>
</svg>
</button>
<script>
// Toggle early based on <html class="dark">
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (themeToggleDarkIcon != null && themeToggleLightIcon != null) {
let dark = document.documentElement.classList.contains('dark');
const show = dark ? themeToggleDarkIcon : themeToggleLightIcon
const hide = dark ? themeToggleLightIcon : themeToggleDarkIcon
show.classList.remove('hidden', 'text-transparent');
hide.classList.add('hidden', 'text-transparent');
}
</script>
"""
end
end

View file

@ -5,7 +5,7 @@
<img src={~p"/images/logo.svg"} width="36" />
</a>
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
v<%= Application.spec(:exmr, :vsn) %>
v{Application.spec(:exmr, :vsn)}
</p>
</div>
</div>
@ -13,6 +13,6 @@
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl">
<.flash_group flash={@flash} />
<%= @inner_content %>
{@inner_content}
</div>
</main>

View file

@ -4,55 +4,63 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title suffix=" · Phoenix Framework">
<%= assigns[:page_title] || "Exmr" %>
<.live_title>
{assigns[:page_title] || "ExMR"}
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script>
if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark')
}
</script>
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="bg-white">
<body class="bg-white dark:bg-zinc-950 dark:text-zinc-100">
<ul class="relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
<DarkMode.button />
<%= if @current_user do %>
<li class="text-[0.8125rem] leading-6 text-zinc-900">
<%= @current_user.email %>
<li class="text-[0.8125rem] leading-6 text-zinc-900 dark:text-zinc-300">
{@current_user.email}
</li>
<li>
<.link
href={~p"/users/settings"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700 dark:text-zinc-300 dark:hover:text-zinc-100"
>
Settings
{gettext("Settings")}
</.link>
</li>
<li>
<.link
href={~p"/users/log_out"}
method="delete"
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700 dark:text-zinc-300 dark:hover:text-zinc-100"
>
Log out
{gettext("Log out")}
</.link>
</li>
<% else %>
<li>
<.link
href={~p"/users/register"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700 dark:text-zinc-300 dark:hover:text-zinc-100"
>
Register
{gettext("Register")}
</.link>
</li>
<li>
<.link
href={~p"/users/log_in"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700 dark:text-zinc-300 dark:hover:text-zinc-100"
>
Log in
{gettext("Log in")}
</.link>
</li>
<% end %>
</ul>
<%= @inner_content %>
{@inner_content}
</body>
</html>

View file

@ -44,25 +44,25 @@
<h1 class="mt-10 flex items-center text-sm font-semibold leading-6 text-purple-400">
ExMR
<small class="bg-brand/5 text-[0.8125rem] ml-3 rounded-full px-2 font-medium leading-6 text-purple-400">
v<%= Application.spec(:exmr, :vsn) %>
v{Application.spec(:exmr, :vsn)}
</small>
</h1>
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-zinc-900 text-balance">
hell yeah
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-zinc-900 dark:text-zinc-200/90 text-balance">
{gettext("A simple, modern, and fast exam management system.")}
</p>
<p class="mt-4 text-base leading-7 text-zinc-600">
y'all really though i'm gonna be serious? lol
<p class="mt-4 text-base leading-7 text-zinc-600 dark:text-zinc-300">
{gettext("Built using Phoenix LiveView, Ecto, and TailwindCSS by @vavakado")}
</p>
<div class="flex">
<div class="w-full sm:w-auto">
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
<a
href="https://github.com/phoenixframework/phoenix"
href="https://git.vavakado.xyz/vavakado/exmr"
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
>
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 dark:bg-zinc-800 dark:group-hover:bg-zinc-900/95 sm:group-hover:scale-105">
</span>
<span class="relative flex items-center gap-4 sm:flex-col">
<span class="relative flex items-center gap-4 sm:flex-col dark:text-white">
<svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
<path
fill-rule="evenodd"
@ -79,9 +79,9 @@
href="https://mas.to/@vavakado"
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
>
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105 dark:bg-zinc-800 dark:group-hover:bg-zinc-900/95">
</span>
<span class="relative flex items-center gap-4 sm:flex-col">
<span class="relative flex items-center gap-4 sm:flex-col dark:text-white">
<svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a4 4 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522q0-1.288.66-2.046c.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764q.662.757.661 2.046z" />
</svg>
@ -93,7 +93,7 @@
</div>
<div class="text-center py-8 font-bold text-3xl text-red-600">
<.link navigate={~p"/exams"} class="underline hover:text-red-400 transition-all ease-in-out">
exams
{gettext("Exams")}
</.link>
</div>
</div>

7
lib/exmr_web/helpers.ex Normal file
View file

@ -0,0 +1,7 @@
defmodule ExmrWeb.LiveHelpers do
def on_mount(:default, _params, session, socket) do
locale = session["locale"] || "en"
Gettext.put_locale(locale)
{:cont, socket}
end
end

View file

@ -8,8 +8,10 @@ defmodule ExmrWeb.ExamLive.FormComponent do
~H"""
<div>
<.header>
<%= @title %>
<:subtitle>Use this form to manage exam records in your database.</:subtitle>
{@title}
<:subtitle>
{gettext("Use this form to manage exam records in your database.")}
</:subtitle>
</.header>
<.simple_form
@ -19,11 +21,11 @@ defmodule ExmrWeb.ExamLive.FormComponent do
phx-change="validate"
phx-submit="save"
>
<.input field={@form[:subject]} type="text" label="Subject" />
<.input field={@form[:description]} type="text" label="Description" />
<.input field={@form[:date]} type="date" label="Date" />
<.input field={@form[:subject]} type="text" label={gettext("Subject")} />
<.input field={@form[:description]} type="text" label={gettext("Description")} />
<.input field={@form[:date]} type="date" label={gettext("Date")} />
<:actions>
<.button phx-disable-with="Saving...">Save Exam</.button>
<.button phx-disable-with={gettext("Saving...")}>{gettext("Save Exam")}</.button>
</:actions>
</.simple_form>
</div>

View file

@ -12,6 +12,8 @@ defmodule ExmrWeb.ExamLive.Index do
|> assign(:sort_by, "date")
|> assign(:live_action, :index)
|> assign(exam: %{})
|> assign(events: Enum.group_by(Exams.list_exams(), & &1.date))
|> assign(current_month: Date.utc_today())
{:ok, socket}
end
@ -29,7 +31,7 @@ defmodule ExmrWeb.ExamLive.Index do
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New Exam")
|> assign(:page_title, gettext("New Exam"))
|> assign(:exam, %Exam{})
end

View file

@ -1,14 +1,27 @@
<.header>
Listing Exams
{gettext("Listing Exams")}
<div>
<button phx-click="sort" phx-value-by="subject" class="font-light">Sort by Subject</button>
<a>|</a>
<button phx-click="sort" phx-value-by="date" class="font-light">Sort by Date</button>
<button
phx-click="sort"
phx-value-by="subject"
class="font-light text-xs md:text-sm lg:text-base"
>
{gettext("Sort by Subject")}
</button>
<a class="invisible lg:visible">|</a>
<button
phx-click="sort"
phx-value-by="date"
class="font-light text-xs md:text-sm lg:text-base"
>
{gettext("Sort by Date")}
</button>
</div>
<:actions>
<.link patch={~p"/exams/new"}>
<.button>New Exam</.button>
<.button class="dark:bg-sky-300 text-white dark:text-black/80 dark:hover:bg-sky-400">
{gettext("New Exam")}
</.button>
</.link>
</:actions>
</.header>
@ -16,22 +29,32 @@
<div class="divide-y">
<div :for={exam <- @exams} class="py-2 flex gap-2">
<div class="grow">
<div class="font-bold"><%= exam.subject %></div>
<div class="font-sm"><%= exam.date %></div>
<div class="font-bold text-xs md:text-sm lg:text-base">{exam.subject}</div>
<div class="text-xs md:text-sm lg:text-base">
{exam.date} |
<b class="text-xs md:text-sm lg:text-base">
{case Date.diff(exam.date, Date.utc_today()) do
0 -> gettext("Today")
1 -> gettext("Tommorow")
x when x > 1 -> ngettext("Tomorrow", "%{count} days left", x)
x when x < 0 -> ngettext("Yesterday", "%{count} days passed", x * -1)
end}
</b>
</div>
</div>
<button
phx-click="edit"
phx-value-id={exam.id}
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-3 rounded-md"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-1 text-xs md:text-sm lg:py-2 lg:px-3 rounded-md"
>
Edit
{gettext("Edit")}
</button>
<button
phx-click="remove"
phx-value-id={exam.id}
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-3 rounded-md"
class="bg-red-500 hover:bg-red-700 text-white font-bold p-1 text-xs md:text-sm lg:py-2 lg:px-3 rounded-md"
>
Remove
{gettext("Remove")}
</button>
</div>
</div>
@ -46,3 +69,54 @@
patch={~p"/exams"}
/>
</.modal>
<div>
<!-- Navigation -->
<!-- <div class="flex justify-between mb-4"> -->
<!-- <button phx-click="previous_month" class="px-2 py-1 bg-blue-200">Previous</button> -->
<!-- <h2>{@current_month |> Date.to_string() |> String.replace("-", " ")}</h2> -->
<!-- <button phx-click="next_month" class="px-2 py-1 bg-blue-200">Next</button> -->
<!-- </div> -->
<!-- Calendar -->
<div class="grid grid-cols-7 gap-1">
<!-- Weekdays -->
<%= for day <- ~w(#{gettext("Sun")} #{gettext("Mon")} #{gettext("Tue")} #{gettext("Wed")} #{gettext("Thu")}
#{gettext("Fri")} #{gettext("Sat")}) do %>
<div class="font-bold text-center">{day}</div>
<% end %>
<!-- Empty spaces for previous month -->
<%= for _ <- 1..(Date.day_of_week(@current_month)) do %>
<div></div>
<% end %>
<!-- Days of the month -->
<%= for day <- 1..Date.days_in_month(@current_month) do %>
<% {:ok, date} = Date.new(@current_month.year, @current_month.month, day) %>
<div class={[
"border rounded p-0.5 md:p-2 lg:p-3 w-full h-12 md:h-20 hover:scale-125 transition-transform ease-in-out duration-100",
@current_month == date &&
"bg-zinc-200 dark:bg-zinc-800 border-green-500 dark:border-green-300",
@current_month != date &&
"bg-white dark:bg-zinc-950 dark:border-zinc-400 "
]}>
<div class="text-xs md:text-sm lg:text-base font-bold">{day}</div>
<!-- Render events for the current day -->
<%= case Map.get(@events, date) do %>
<% nil -> %>
<!-- No events for this day -->
<% events -> %>
<ul class="mt-2 text-xs">
<%= for event <- events do %>
<li>{event.subject}</li>
<% end %>
</ul>
<% end %>
</div>
<% end %>
</div>
</div>

View file

@ -1,6 +1,6 @@
<.header>
exam №<%= @exam.id %>
<:subtitle>очередная контрольная работа по <strong><%= @exam.subject %></strong></:subtitle>
exam №{@exam.id}
<:subtitle>очередная контрольная работа по <strong>{@exam.subject}</strong></:subtitle>
<:actions>
<.link patch={~p"/exams/#{@exam}/show/edit"} phx-click={JS.push_focus()}>
<.button>изменить</.button>
@ -9,8 +9,8 @@
</.header>
<.list>
<:item title="Description"><%= @exam.description %></:item>
<:item title="Date"><%= @exam.date %></:item>
<:item title="Description">{@exam.description}</:item>
<:item title="Date">{@exam.date}</:item>
</.list>
<.back navigate={~p"/exams"}>Back to exams</.back>

View file

@ -14,7 +14,7 @@ defmodule ExmrWeb.UserForgotPasswordLive do
<.simple_form for={@form} id="reset_password_form" phx-submit="send_email">
<.input field={@form[:email]} type="email" placeholder="Email" required />
<:actions>
<.button phx-disable-with="Sending..." class="w-full">
<.button phx-disable-with="Sending..." class="w-full dark:text-black dark:bg-red-600">
Send password reset instructions
</.button>
</:actions>

View file

@ -26,7 +26,7 @@ defmodule ExmrWeb.UserLoginLive do
</.link>
</:actions>
<:actions>
<.button phx-disable-with="Logging in..." class="w-full">
<.button phx-disable-with="Logging in..." class="w-full dark:bg-teal-600">
Log in <span aria-hidden="true"></span>
</.button>
</:actions>

View file

@ -6,7 +6,7 @@ defmodule ExmrWeb.UserRegistrationLive do
def render(assigns) do
~H"""
<div class="mx-auto max-w-sm">
<div class="mx-auto max-w-sm dark:bg-zinc-950">
<.header class="text-center">
Register for an account
<:subtitle>
@ -35,7 +35,9 @@ defmodule ExmrWeb.UserRegistrationLive do
<.input field={@form[:password]} type="password" label="Password" required />
<:actions>
<.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
<.button phx-disable-with="Creating account..." class="w-full dark:bg-green-800">
Create an account
</.button>
</:actions>
</.simple_form>
</div>

View file

@ -6,8 +6,8 @@ defmodule ExmrWeb.UserSettingsLive do
def render(assigns) do
~H"""
<.header class="text-center">
Account Settings
<:subtitle>Manage your account email address and password settings</:subtitle>
{gettext("Account Settings")}
<:subtitle>{gettext("Manage your account email address and password settings")}</:subtitle>
</.header>
<div class="space-y-12 divide-y">
@ -18,18 +18,20 @@ defmodule ExmrWeb.UserSettingsLive do
phx-submit="update_email"
phx-change="validate_email"
>
<.input field={@email_form[:email]} type="email" label="Email" required />
<.input field={@email_form[:email]} type="email" label={gettext("Email")} required />
<.input
field={@email_form[:current_password]}
name="current_password"
id="current_password_for_email"
type="password"
label="Current password"
label={gettext("Current password")}
value={@email_form_current_password}
required
/>
<:actions>
<.button phx-disable-with="Changing...">Change Email</.button>
<.button phx-disable-with={gettext("Changing...")}>
{gettext("Change Email")}
</.button>
</:actions>
</.simple_form>
</div>
@ -49,23 +51,30 @@ defmodule ExmrWeb.UserSettingsLive do
id="hidden_user_email"
value={@current_email}
/>
<.input field={@password_form[:password]} type="password" label="New password" required />
<.input
field={@password_form[:password]}
type="password"
label={gettext("New password")}
required
/>
<.input
field={@password_form[:password_confirmation]}
type="password"
label="Confirm new password"
label={gettext("Confirm new password")}
/>
<.input
field={@password_form[:current_password]}
name="current_password"
type="password"
label="Current password"
label={gettext("Current password")}
id="current_password_for_password"
value={@current_password}
required
/>
<:actions>
<.button phx-disable-with="Changing...">Change Password</.button>
<.button phx-disable-with={gettext("Changing...")}>
{gettext("Change Password")}
</.button>
</:actions>
</.simple_form>
</div>
@ -77,10 +86,10 @@ defmodule ExmrWeb.UserSettingsLive do
socket =
case Users.update_user_email(socket.assigns.current_user, token) do
:ok ->
put_flash(socket, :info, "Email changed successfully.")
put_flash(socket, :info, gettext("Email changed successfully."))
:error ->
put_flash(socket, :error, "Email change link is invalid or it has expired.")
put_flash(socket, :error, gettext("Email change link is invalid or it has expired."))
end
{:ok, push_navigate(socket, to: ~p"/users/settings")}
@ -127,7 +136,7 @@ defmodule ExmrWeb.UserSettingsLive do
&url(~p"/users/settings/confirm_email/#{&1}")
)
info = "A link to confirm your email change has been sent to the new address."
info = gettext("A link to confirm your email change has been sent to the new address.")
{:noreply, socket |> put_flash(:info, info) |> assign(email_form_current_password: nil)}
{:error, changeset} ->

View file

@ -0,0 +1,67 @@
defmodule ExmrWeb.Plugs.Locale do
import Plug.Conn
def init(_opts), do: nil
def call(conn, _opts) do
accepted_languages = extract_accept_language(conn)
known_locales = Gettext.known_locales(ExmrWeb.Gettext)
accepted_languages =
known_locales --
known_locales -- accepted_languages
case accepted_languages do
[locale | _] ->
Gettext.put_locale(ExmrWeb.Gettext, locale)
conn
|> put_session(:locale, locale)
_ ->
conn
end
end
# Copied from
# https://raw.githubusercontent.com/smeevil/set_locale/fd35624e25d79d61e70742e42ade955e5ff857b8/lib/headers.ex
def extract_accept_language(conn) do
case Plug.Conn.get_req_header(conn, "accept-language") do
[value | _] ->
value
|> String.split(",")
|> Enum.map(&parse_language_option/1)
|> Enum.sort(&(&1.quality > &2.quality))
|> Enum.map(& &1.tag)
|> Enum.reject(&is_nil/1)
|> ensure_language_fallbacks()
_ ->
[]
end
end
defp parse_language_option(string) do
captures = Regex.named_captures(~r/^\s?(?<tag>[\w\-]+)(?:;q=(?<quality>[\d\.]+))?$/i, string)
quality =
case Float.parse(captures["quality"] || "1.0") do
{val, _} -> val
_ -> 1.0
end
%{tag: captures["tag"], quality: quality}
end
defp ensure_language_fallbacks(tags) do
Enum.flat_map(tags, fn tag ->
case String.split(tag, "-") do
[language, _country_variant] ->
if Enum.member?(tags, language), do: [tag], else: [tag, language]
[_language] ->
[tag]
end
end)
end
end

View file

@ -12,6 +12,7 @@ defmodule ExmrWeb.Router do
plug :put_root_layout, html: {ExmrWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug ExmrWeb.Plugs.Locale
plug :fetch_current_user
end
@ -53,7 +54,7 @@ defmodule ExmrWeb.Router do
pipe_through [:browser, :redirect_if_user_is_authenticated]
live_session :redirect_if_user_is_authenticated,
on_mount: [{ExmrWeb.UserAuth, :redirect_if_user_is_authenticated}] do
on_mount: [{ExmrWeb.UserAuth, :redirect_if_user_is_authenticated}, ExmrWeb.LiveHelpers] do
if Exmr.enable_registration() != "false" do
live "/users/register", UserRegistrationLive, :new
end
@ -70,7 +71,7 @@ defmodule ExmrWeb.Router do
pipe_through [:browser, :require_authenticated_user]
live_session :require_authenticated_user,
on_mount: [{ExmrWeb.UserAuth, :ensure_authenticated}] do
on_mount: [{ExmrWeb.UserAuth, :ensure_authenticated}, ExmrWeb.LiveHelpers] do
if Exmr.enable_registration() == "false" do
live "/users/register", UserRegistrationLive, :new
end
@ -93,7 +94,7 @@ defmodule ExmrWeb.Router do
delete "/users/log_out", UserSessionController, :delete
live_session :current_user,
on_mount: [{ExmrWeb.UserAuth, :mount_current_user}] do
on_mount: [{ExmrWeb.UserAuth, :mount_current_user}, ExmrWeb.LiveHelpers] do
live "/users/confirm/:token", UserConfirmationLive, :edit
live "/users/confirm", UserConfirmationInstructionsLive, :new
end

View file

@ -4,7 +4,7 @@ defmodule Exmr.MixProject do
def project do
[
app: :exmr,
version: "0.1.0",
version: "0.2.1",
elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
@ -40,8 +40,7 @@ defmodule Exmr.MixProject do
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:phoenix_html, "~> 4.1"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
# TODO bump on release to {:phoenix_live_view, "~> 1.0.0"},
{:phoenix_live_view, "~> 1.0.0-rc.1", override: true},
{:phoenix_live_view, "~> 1.0.0"},
{:floki, ">= 0.30.0", only: :test},
{:phoenix_live_dashboard, "~> 0.8.3"},
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},

View file

@ -1,5 +1,5 @@
%{
"bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"},
"bandit": {:hex, :bandit, "1.6.1", "9e01b93d72ddc21d8c576a704949e86ee6cde7d11270a1d3073787876527a48f", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5a904bf010ea24b67979835e0507688e31ac873d4ffc8ed0e5413e8d77455031"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.0", "feab711974beba4cb348147170346fe097eea2e840db4e012a145e180ed4ab75", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "563e92a6c77d667b19c5f4ba17ab6d440a085696bdf4c68b9b0f5b30bc5422b8"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"},
@ -15,21 +15,21 @@
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"},
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
"hpax": {:hex, :hpax, "1.0.1", "c857057f89e8bd71d97d9042e009df2a42705d6d690d54eca84c8b29af0787b0", [:mix], [], "hexpm", "4e2d5a4f76ae1e3048f35ae7adb1641c36265510a2d4638157fbcb53dda38445"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
"phoenix": {:hex, :phoenix, "1.7.18", "5310c21443514be44ed93c422e15870aef254cf1b3619e4f91538e7529d2b2e4", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "1797fcc82108442a66f2c77a643a62980f342bfeb63d6c9a515ab8294870004e"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"},
"phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.5", "d5f44d7dbd7cfacaa617b70c5a14b2b598d6f93b9caa8e350c51d56cd4350a9b", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1d73920515554d7d6c548aee0bf10a4780568b029d042eccb336db29ea0dad70"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.7", "d2abca526422adea88896769529addb6443390b1d4f1ff9cbe694312d8875fb2", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b82a4575f6f3eb5b97922ec6874b0c52b3ca0cc5dcb4b14ddc478cbfa135dd01"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0", "3a10dfce8f87b2ad4dc65de0732fc2a11e670b2779a19e8d3281f4619a85bce4", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "254caef0028765965ca6bd104cc7d68dcc7d57cc42912bef92f6b03047251d99"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
@ -40,7 +40,7 @@
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
"thousand_island": {:hex, :thousand_island, "1.3.7", "1da7598c0f4f5f50562c097a3f8af308ded48cd35139f0e6f17d9443e4d0c9c5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0139335079953de41d381a6134d8b618d53d084f558c734f2662d1a72818dd12"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
}

270
priv/gettext/default.pot Normal file
View file

@ -0,0 +1,270 @@
## This file is a PO Template file.
##
## "msgid"s here are often extracted from source code.
## Add new messages manually only if they're dynamic
## messages that can't be statically extracted.
##
## Run "mix gettext.extract" to bring this file up to
## date. Leave "msgstr"s empty as changing them here has no
## effect: edit them in PO (.po) files instead.
#
msgid ""
msgstr ""
#: lib/exmr_web/controllers/page_html/home.html.heex:51
#, elixir-autogen, elixir-format
msgid "A simple, modern, and fast exam management system."
msgstr ""
#: lib/exmr_web/components/core_components.ex:489
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
#: lib/exmr_web/components/core_components.ex:164
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr ""
#: lib/exmr_web/controllers/page_html/home.html.heex:54
#, elixir-autogen, elixir-format
msgid "Built using Phoenix LiveView, Ecto, and TailwindCSS by @vavakado"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:50
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""
#: lib/exmr_web/components/core_components.ex:155
#, elixir-autogen, elixir-format
msgid "Error!"
msgstr ""
#: lib/exmr_web/controllers/page_html/home.html.heex:96
#, elixir-autogen, elixir-format
msgid "Exams"
msgstr ""
#: lib/exmr_web/components/core_components.ex:176
#, elixir-autogen, elixir-format
msgid "Hang in there while we get back on track"
msgstr ""
#: lib/exmr_web/live/exam_live/index.ex:34
#: lib/exmr_web/live/exam_live/index.html.heex:23
#, elixir-autogen, elixir-format
msgid "New Exam"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:57
#, elixir-autogen, elixir-format
msgid "Remove"
msgstr ""
#: lib/exmr_web/components/core_components.ex:171
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:17
#, elixir-autogen, elixir-format
msgid "Sort by Date"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:9
#, elixir-autogen, elixir-format
msgid "Sort by Subject"
msgstr ""
#: lib/exmr_web/components/core_components.ex:154
#, elixir-autogen, elixir-format
msgid "Success!"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:37
#, elixir-autogen, elixir-format
msgid "Today"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:39
#, elixir-autogen, elixir-format
msgid "Tomorrow"
msgid_plural "%{count} days left"
msgstr[0] ""
msgstr[1] ""
#: lib/exmr_web/components/core_components.ex:159
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr ""
#: lib/exmr_web/components/core_components.ex:80
#: lib/exmr_web/components/core_components.ex:134
#, elixir-autogen, elixir-format
msgid "close"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:139
#, elixir-autogen, elixir-format
msgid "A link to confirm your email change has been sent to the new address."
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:9
#, elixir-autogen, elixir-format
msgid "Account Settings"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:33
#, elixir-autogen, elixir-format
msgid "Change Email"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:76
#, elixir-autogen, elixir-format
msgid "Change Password"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:32
#: lib/exmr_web/live/user_settings_live.ex:75
#, elixir-autogen, elixir-format
msgid "Changing..."
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:63
#, elixir-autogen, elixir-format
msgid "Confirm new password"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:27
#: lib/exmr_web/live/user_settings_live.ex:69
#, elixir-autogen, elixir-format
msgid "Current password"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:21
#, elixir-autogen, elixir-format
msgid "Email"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:92
#, elixir-autogen, elixir-format
msgid "Email change link is invalid or it has expired."
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:89
#, elixir-autogen, elixir-format
msgid "Email changed successfully."
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:42
#, elixir-autogen, elixir-format
msgid "Log out"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:10
#, elixir-autogen, elixir-format
msgid "Manage your account email address and password settings"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:57
#, elixir-autogen, elixir-format
msgid "New password"
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:33
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:26
#, elixir-autogen, elixir-format
msgid "Date"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:25
#, elixir-autogen, elixir-format
msgid "Description"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:24
#, elixir-autogen, elixir-format
msgid "Subject"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:13
#, elixir-autogen, elixir-format
msgid "Use this form to manage exam records in your database."
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:2
#, elixir-autogen, elixir-format
msgid "Listing Exams"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:28
#, elixir-autogen, elixir-format
msgid "Save Exam"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:28
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:40
#, elixir-autogen, elixir-format
msgid "Yesterday"
msgid_plural "%{count} days passed"
msgstr[0] ""
msgstr[1] ""
#: lib/exmr_web/live/exam_live/index.html.heex:38
#, elixir-autogen, elixir-format
msgid "Tommorow"
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:59
#, elixir-autogen, elixir-format
msgid "Log in"
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:51
#, elixir-autogen, elixir-format
msgid "Register"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:85
#, elixir-autogen, elixir-format
msgid "Fri"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Mon"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:85
#, elixir-autogen, elixir-format
msgid "Sat"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Sun"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Thu"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Tue"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Wed"
msgstr ""

View file

@ -0,0 +1,270 @@
## "msgid"s in this file come from POT (.pot) files.
###
### Do not add, change, or remove "msgid"s manually here as
### they're tied to the ones in the corresponding POT file
### (with the same domain).
###
### Use "mix gettext.extract --merge" or "mix gettext.merge"
### to merge POT files into PO files.
msgid ""
msgstr ""
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: lib/exmr_web/controllers/page_html/home.html.heex:51
#, elixir-autogen, elixir-format
msgid "A simple, modern, and fast exam management system."
msgstr ""
#: lib/exmr_web/components/core_components.ex:489
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
#: lib/exmr_web/components/core_components.ex:164
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr ""
#: lib/exmr_web/controllers/page_html/home.html.heex:54
#, elixir-autogen, elixir-format
msgid "Built using Phoenix LiveView, Ecto, and TailwindCSS by @vavakado"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:50
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""
#: lib/exmr_web/components/core_components.ex:155
#, elixir-autogen, elixir-format
msgid "Error!"
msgstr ""
#: lib/exmr_web/controllers/page_html/home.html.heex:96
#, elixir-autogen, elixir-format
msgid "Exams"
msgstr ""
#: lib/exmr_web/components/core_components.ex:176
#, elixir-autogen, elixir-format
msgid "Hang in there while we get back on track"
msgstr ""
#: lib/exmr_web/live/exam_live/index.ex:34
#: lib/exmr_web/live/exam_live/index.html.heex:23
#, elixir-autogen, elixir-format
msgid "New Exam"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:57
#, elixir-autogen, elixir-format
msgid "Remove"
msgstr ""
#: lib/exmr_web/components/core_components.ex:171
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:17
#, elixir-autogen, elixir-format
msgid "Sort by Date"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:9
#, elixir-autogen, elixir-format
msgid "Sort by Subject"
msgstr ""
#: lib/exmr_web/components/core_components.ex:154
#, elixir-autogen, elixir-format
msgid "Success!"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:37
#, elixir-autogen, elixir-format
msgid "Today"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:39
#, elixir-autogen, elixir-format
msgid "Tomorrow"
msgid_plural "%{count} days left"
msgstr[0] ""
msgstr[1] ""
#: lib/exmr_web/components/core_components.ex:159
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr ""
#: lib/exmr_web/components/core_components.ex:80
#: lib/exmr_web/components/core_components.ex:134
#, elixir-autogen, elixir-format
msgid "close"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:139
#, elixir-autogen, elixir-format
msgid "A link to confirm your email change has been sent to the new address."
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:9
#, elixir-autogen, elixir-format
msgid "Account Settings"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:33
#, elixir-autogen, elixir-format
msgid "Change Email"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:76
#, elixir-autogen, elixir-format
msgid "Change Password"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:32
#: lib/exmr_web/live/user_settings_live.ex:75
#, elixir-autogen, elixir-format
msgid "Changing..."
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:63
#, elixir-autogen, elixir-format
msgid "Confirm new password"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:27
#: lib/exmr_web/live/user_settings_live.ex:69
#, elixir-autogen, elixir-format
msgid "Current password"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:21
#, elixir-autogen, elixir-format
msgid "Email"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:92
#, elixir-autogen, elixir-format
msgid "Email change link is invalid or it has expired."
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:89
#, elixir-autogen, elixir-format
msgid "Email changed successfully."
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:42
#, elixir-autogen, elixir-format
msgid "Log out"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:10
#, elixir-autogen, elixir-format
msgid "Manage your account email address and password settings"
msgstr ""
#: lib/exmr_web/live/user_settings_live.ex:57
#, elixir-autogen, elixir-format
msgid "New password"
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:33
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:26
#, elixir-autogen, elixir-format
msgid "Date"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:25
#, elixir-autogen, elixir-format
msgid "Description"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:24
#, elixir-autogen, elixir-format
msgid "Subject"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:13
#, elixir-autogen, elixir-format
msgid "Use this form to manage exam records in your database."
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:2
#, elixir-autogen, elixir-format
msgid "Listing Exams"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:28
#, elixir-autogen, elixir-format, fuzzy
msgid "Save Exam"
msgstr ""
#: lib/exmr_web/live/exam_live/form_component.ex:28
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:40
#, elixir-autogen, elixir-format
msgid "Yesterday"
msgid_plural "%{count} days passed"
msgstr[0] ""
msgstr[1] ""
#: lib/exmr_web/live/exam_live/index.html.heex:38
#, elixir-autogen, elixir-format, fuzzy
msgid "Tommorow"
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:59
#, elixir-autogen, elixir-format
msgid "Log in"
msgstr ""
#: lib/exmr_web/components/layouts/root.html.heex:51
#, elixir-autogen, elixir-format
msgid "Register"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:85
#, elixir-autogen, elixir-format
msgid "Fri"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Mon"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:85
#, elixir-autogen, elixir-format
msgid "Sat"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Sun"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Thu"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Tue"
msgstr ""
#: lib/exmr_web/live/exam_live/index.html.heex:84
#, elixir-autogen, elixir-format
msgid "Wed"
msgstr ""

Binary file not shown.

View file

@ -0,0 +1,242 @@
# # This file is a PO Template file.
# #
# # "msgid"s here are often extracted from source code.
# # Add new messages manually only if they're dynamic
# # messages that can't be statically extracted.
# #
# # Run "mix gettext.extract" to bring this file up to
# # date. Leave "msgstr"s empty as changing them here has no
# # effect: edit them in PO (.po) files instead.
# Vladimir Rubin <vavakado@proton.me>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: unnamed project\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: 2024-12-23 19:01+0200\n"
"Last-Translator: Vladimir Rubin <vavakado@proton.me>\n"
"Language-Team: Russian\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Gtranslator 47.1\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: lib/exmr_web/controllers/page_html/home.html.heex:51
msgid "A simple, modern, and fast exam management system."
msgstr "Простая, современная и быстрая система управления экзаменами."
#: lib/exmr_web/components/core_components.ex:489
msgid "Actions"
msgstr "Действия"
#: lib/exmr_web/components/core_components.ex:164
msgid "Attempting to reconnect"
msgstr "Попытка восстановить соединение"
#: lib/exmr_web/controllers/page_html/home.html.heex:54
msgid "Built using Phoenix LiveView, Ecto, and TailwindCSS by @vavakado"
msgstr ""
"Создано с использованием Phoenix LiveView, Ecto и TailwindCSS в исполнении "
"@vavakado"
#: lib/exmr_web/live/exam_live/index.html.heex:50
msgid "Edit"
msgstr "Изменить"
#: lib/exmr_web/components/core_components.ex:155
msgid "Error!"
msgstr "Ошибка!"
#: lib/exmr_web/controllers/page_html/home.html.heex:96
msgid "Exams"
msgstr "Экзамены"
#: lib/exmr_web/components/core_components.ex:176
msgid "Hang in there while we get back on track"
msgstr "Держитесь, пока мы не вернемся в строй"
#: lib/exmr_web/live/exam_live/index.ex:34
#: lib/exmr_web/live/exam_live/index.html.heex:23
msgid "New Exam"
msgstr "Новый экзамен"
#: lib/exmr_web/live/exam_live/index.html.heex:57
msgid "Remove"
msgstr "Удалить"
#: lib/exmr_web/components/core_components.ex:171
msgid "Something went wrong!"
msgstr "Что-то пошло не так!"
#: lib/exmr_web/live/exam_live/index.html.heex:17
msgid "Sort by Date"
msgstr "Сортировать по дате"
#: lib/exmr_web/live/exam_live/index.html.heex:9
msgid "Sort by Subject"
msgstr "Сортировать по предмету"
#: lib/exmr_web/components/core_components.ex:154
msgid "Success!"
msgstr "Успех!"
#: lib/exmr_web/live/exam_live/index.html.heex:37
msgid "Today"
msgstr "Сегодня"
#: lib/exmr_web/live/exam_live/index.html.heex:39
msgid "Tomorrow"
msgid_plural "%{count} days left"
msgstr[0] "%{count} день остался"
msgstr[1] "%{count} дня осталось"
msgstr[2] "%{count} дней осталось"
#: lib/exmr_web/components/core_components.ex:159
msgid "We can't find the internet"
msgstr "Мы не можем найти интернет"
#: lib/exmr_web/components/core_components.ex:80
#: lib/exmr_web/components/core_components.ex:134
msgid "close"
msgstr "закрыть"
#: lib/exmr_web/live/user_settings_live.ex:139
msgid "A link to confirm your email change has been sent to the new address."
msgstr ""
"На новый адрес отправлена ссылка для подтверждения изменения электронной "
"почты."
#: lib/exmr_web/live/user_settings_live.ex:9
msgid "Account Settings"
msgstr "Настройки аккаунта"
#: lib/exmr_web/live/user_settings_live.ex:33
msgid "Change Email"
msgstr "Изменить электронную почту"
#: lib/exmr_web/live/user_settings_live.ex:76
msgid "Change Password"
msgstr "Изменить пароль"
#: lib/exmr_web/live/user_settings_live.ex:32
#: lib/exmr_web/live/user_settings_live.ex:75
msgid "Changing..."
msgstr "Меняется..."
#: lib/exmr_web/live/user_settings_live.ex:63
msgid "Confirm new password"
msgstr "Подтвердите новый пароль"
#: lib/exmr_web/live/user_settings_live.ex:27
#: lib/exmr_web/live/user_settings_live.ex:69
msgid "Current password"
msgstr "Текущий пароль"
#: lib/exmr_web/live/user_settings_live.ex:21
msgid "Email"
msgstr "Электронная почта"
#: lib/exmr_web/live/user_settings_live.ex:92
msgid "Email change link is invalid or it has expired."
msgstr ""
"Ссылка для изменения электронной почты недействительна или срок ее действия "
"истек."
#: lib/exmr_web/live/user_settings_live.ex:89
msgid "Email changed successfully."
msgstr "Электронная почта успешно изменена."
#: lib/exmr_web/components/layouts/root.html.heex:42
msgid "Log out"
msgstr "Выйти из системы"
#: lib/exmr_web/live/user_settings_live.ex:10
msgid "Manage your account email address and password settings"
msgstr "Управление адресом электронной почты и настройками пароля"
#: lib/exmr_web/live/user_settings_live.ex:57
msgid "New password"
msgstr "Новый пароль"
#: lib/exmr_web/components/layouts/root.html.heex:33
msgid "Settings"
msgstr "Настройки"
#: lib/exmr_web/live/exam_live/form_component.ex:26
msgid "Date"
msgstr "Дата"
#: lib/exmr_web/live/exam_live/form_component.ex:25
msgid "Description"
msgstr "Описание"
#: lib/exmr_web/live/exam_live/form_component.ex:24
msgid "Subject"
msgstr "Предмет"
#: lib/exmr_web/live/exam_live/form_component.ex:13
msgid "Use this form to manage exam records in your database."
msgstr ""
"Используйте эту форму для управления записями экзаменов в вашей базе данных."
#: lib/exmr_web/live/exam_live/index.html.heex:2
msgid "Listing Exams"
msgstr "Список Экзаменов"
#: lib/exmr_web/live/exam_live/form_component.ex:28
msgid "Save Exam"
msgstr "Сохранить экзамен"
#: lib/exmr_web/live/exam_live/form_component.ex:28
msgid "Saving..."
msgstr "Сохранение..."
#: lib/exmr_web/live/exam_live/index.html.heex:40
msgid "Yesterday"
msgid_plural "%{count} days passed"
msgstr[0] "%{count} день прошёл"
msgstr[1] "%{count} дня прошло"
msgstr[2] "%{count} дней прошло"
#: lib/exmr_web/live/exam_live/index.html.heex:38
msgid "Tommorow"
msgstr "Завтра"
#: lib/exmr_web/components/layouts/root.html.heex:59
msgid "Log in"
msgstr "Зайти в аккаунт"
#: lib/exmr_web/components/layouts/root.html.heex:51
msgid "Register"
msgstr "Зарегестрироваться"
#: lib/exmr_web/live/exam_live/index.html.heex:85
msgid "Fri"
msgstr "Пт"
#: lib/exmr_web/live/exam_live/index.html.heex:84
msgid "Mon"
msgstr "Пн"
#: lib/exmr_web/live/exam_live/index.html.heex:85
msgid "Sat"
msgstr "Сб"
#: lib/exmr_web/live/exam_live/index.html.heex:84
msgid "Sun"
msgstr "Вс"
#: lib/exmr_web/live/exam_live/index.html.heex:84
msgid "Thu"
msgstr "Чт"
#: lib/exmr_web/live/exam_live/index.html.heex:84
msgid "Tue"
msgstr "Вт"
#: lib/exmr_web/live/exam_live/index.html.heex:84
msgid "Wed"
msgstr "Ср"

View file

@ -0,0 +1,122 @@
# # "msgid"s in this file come from POT (.pot) files.
# ##
# ## Do not add, change, or remove "msgid"s manually here as
# ## they're tied to the ones in the corresponding POT file
# ## (with the same domain).
# ##
# ## Use "mix gettext.extract --merge" or "mix gettext.merge"
# ## to merge POT files into PO files.
# Vladimir Rubin <vavakado@proton.me>, 2024.
#
msgid ""
msgstr ""
"Language: ru\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"MIME-Version: 1.0\n"
"Project-Id-Version: unnamed project\n"
"Last-Translator: Vladimir Rubin <vavakado@proton.me>\n"
"Language-Team: Russian\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2024-12-12 18:52+0200\n"
"X-Generator: Gtranslator 47.1\n"
msgid "can't be blank"
msgstr "не может быть пустым"
msgid "has already been taken"
msgstr "уже забрали"
msgid "is invalid"
msgstr "недействителен"
msgid "must be accepted"
msgstr "должен быть принят"
msgid "has invalid format"
msgstr "имеет неправильный формат"
msgid "has an invalid entry"
msgstr "имеет недопустимую запись"
msgid "is reserved"
msgstr "зарезервирован"
msgid "does not match confirmation"
msgstr "не соответствует подтверждению"
msgid "is still associated with this entry"
msgstr "по-прежнему связана с этой записью"
msgid "are still associated with this entry"
msgstr "по-прежнему связаны с этой записью"
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] "должен иметь %{count} элементов"
msgstr[1] "должен иметь %{count} элемента"
msgstr[2] "должен иметь %{count} элементов"
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] "должен быть %{count} символов"
msgstr[1] "должен быть %{count} символа"
msgstr[2] "должен быть %{count} символов"
msgid "should be %{count} byte(s)"
msgid_plural "should be %{count} byte(s)"
msgstr[0] "должно быть %{count} байт"
msgstr[1] "должно быть %{count} байта"
msgstr[2] "должно быть %{count} байтов"
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] "должно быть не менее %{count} элементов"
msgstr[1] "должно быть не менее %{count} элемента"
msgstr[2] "должно быть не менее %{count} элементов"
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] "должно быть не менее %{count} символов"
msgstr[1] "должно быть не менее %{count} символа"
msgstr[2] "должно быть не менее %{count} символов"
msgid "should be at least %{count} byte(s)"
msgid_plural "should be at least %{count} byte(s)"
msgstr[0] "должно быть хотя бы %{count} байт"
msgstr[1] "должно быть хотя бы %{count} байта"
msgstr[2] "должно быть хотя бы %{count} байтов"
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] "должно содержать не более %{count} элементов"
msgstr[1] "должно содержать не более %{count} элементов"
msgstr[2] "должно содержать не более %{count} элементов"
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] "должно быть не более %{count} символа(ов)"
msgstr[1] "должно быть не более %{count} символа(ов)"
msgstr[2] "должно быть не более %{count} символа(ов)"
msgid "should be at most %{count} byte(s)"
msgid_plural "should be at most %{count} byte(s)"
msgstr[0] "должно быть не более %{count} байт(ов)."
msgstr[1] "должно быть не более %{count} байт(ов)."
msgstr[2] "должно быть не более %{count} байт(ов)."
msgid "must be less than %{number}"
msgstr "должно быть меньше, чем %{number}"
msgid "must be greater than %{number}"
msgstr "должно быть больше, чем %{number}"
msgid "must be less than or equal to %{number}"
msgstr "должно быть меньше или равно %{number}"
msgid "must be greater than or equal to %{number}"
msgstr "должно быть больше или равно %{number}"
msgid "must be equal to %{number}"
msgstr "должно быть равно %{number}"