feat: добавил svg иконки
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 8s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 12s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 25s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 6s

This commit is contained in:
2026-05-11 14:43:14 +03:00
parent a0a0575a99
commit 3b0bbfc858
53 changed files with 1162 additions and 88 deletions
+22
View File
@@ -0,0 +1,22 @@
<!--
tags: [time, watch, clock, ring, alarm, control, operation, function, interface, management]
category: System
version: "1.1"
unicode: "ea04"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 13a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
<path d="M12 10l0 3l2 0" />
<path d="M7 4l-2.75 2" />
<path d="M17 4l2.75 2" />
</svg>

After

Width:  |  Height:  |  Size: 496 B

@@ -0,0 +1,21 @@
<!--
tags: [warning, danger, caution, risk, alert, triangle, control, operation, function, interface]
category: System
version: "1.0"
unicode: "ea06"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 9v4" />
<path d="M10.363 3.591l-8.106 13.534a1.914 1.914 0 0 0 1.636 2.871h16.214a1.914 1.914 0 0 0 1.636 -2.87l-8.106 -13.536a1.914 1.914 0 0 0 -3.274 0" />
<path d="M12 16h.01" />
</svg>

After

Width:  |  Height:  |  Size: 563 B

+20
View File
@@ -0,0 +1,20 @@
<!--
tags: [alarm, sound, notification, bell, control, operation, function, interface, management]
category: System
version: "1.0"
unicode: "ea35"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6" />
<path d="M9 17v1a3 3 0 0 0 6 0v-1" />
</svg>

After

Width:  |  Height:  |  Size: 501 B

+21
View File
@@ -0,0 +1,21 @@
<!--
tags: [read, dictionary, magazine, library, booklet, novel, book, file, paper, text]
category: Document
version: "1.50"
unicode: "efc5"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M19 4v16h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12" />
<path d="M19 16h-12a2 2 0 0 0 -2 2" />
<path d="M9 8h6" />
</svg>

After

Width:  |  Height:  |  Size: 482 B

+25
View File
@@ -0,0 +1,25 @@
<!--
tags: [education, learning, reading, school, library, books, file, paper, text, record]
category: Document
version: "1.52"
unicode: "eff2"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 5a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1l0 -14" />
<path d="M9 5a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1l0 -14" />
<path d="M5 8h4" />
<path d="M9 16h4" />
<path d="M13.803 4.56l2.184 -.53c.562 -.135 1.133 .19 1.282 .732l3.695 13.418a1.02 1.02 0 0 1 -.634 1.219l-.133 .041l-2.184 .53c-.562 .135 -1.133 -.19 -1.282 -.732l-3.695 -13.418a1.02 1.02 0 0 1 .634 -1.219l.133 -.041" />
<path d="M14 9l4 -1" />
<path d="M16 16l3.923 -.98" />
</svg>

After

Width:  |  Height:  |  Size: 872 B

+26
View File
@@ -0,0 +1,26 @@
<!--
tags: [flat, office, city, urban, scyscraper, architecture, construction, building, structure, property]
category: Buildings
version: "1.1"
unicode: "ea4f"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 21l18 0" />
<path d="M9 8l1 0" />
<path d="M9 12l1 0" />
<path d="M9 16l1 0" />
<path d="M14 8l1 0" />
<path d="M14 12l1 0" />
<path d="M14 16l1 0" />
<path d="M5 21v-16a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v16" />
</svg>

After

Width:  |  Height:  |  Size: 610 B

+21
View File
@@ -0,0 +1,21 @@
<!--
category: Design
tags: [energy, power, electricity, creativity, light, idea, bulb]
version: "1.0"
unicode: "ea51"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12h1m8 -9v1m8 8h1m-15.4 -6.4l.7 .7m12.1 -.7l-.7 .7" />
<path d="M9 16a5 5 0 1 1 6 0a3.5 3.5 0 0 0 -1 3a2 2 0 0 1 -4 0a3.5 3.5 0 0 0 -1 -3" />
<path d="M9.7 17l4.6 0" />
</svg>

After

Width:  |  Height:  |  Size: 518 B

@@ -0,0 +1,23 @@
<!--
tags: [date, day, plan, schedule, agenda, calender, calendar, event, control, operation]
category: System
version: "1.1"
unicode: "ea52"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12" />
<path d="M16 3l0 4" />
<path d="M8 3l0 4" />
<path d="M4 11l16 0" />
<path d="M8 15h2v2h-2l0 -2" />
</svg>

After

Width:  |  Height:  |  Size: 558 B

+24
View File
@@ -0,0 +1,24 @@
<!--
tags: [date, day, plan, schedule, agenda, calender, calendar, control, operation, function]
category: System
version: "1.0"
unicode: "ea53"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12" />
<path d="M16 3v4" />
<path d="M8 3v4" />
<path d="M4 11h16" />
<path d="M11 15h1" />
<path d="M12 15v3" />
</svg>

After

Width:  |  Height:  |  Size: 568 B

+22
View File
@@ -0,0 +1,22 @@
<!--
tags: [statistics, diagram, graph, rhythm, data, analysis, chart, bar, visualization, analytics]
category: Charts
version: "1.0"
unicode: "ea59"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 13a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -6" />
<path d="M15 9a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -10" />
<path d="M9 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -14" />
<path d="M4 20h14" />
</svg>

After

Width:  |  Height:  |  Size: 668 B

+20
View File
@@ -0,0 +1,20 @@
<!--
tags: [statistics, diagram, graph, rhythm, data, analysis, chart, line, visualization, analytics]
category: Charts
version: "1.0"
unicode: "ea5c"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 19l16 0" />
<path d="M4 15l4 -6l4 2l4 -5l4 4" />
</svg>

After

Width:  |  Height:  |  Size: 428 B

@@ -0,0 +1,20 @@
<!--
tags: [accept, yes, tick, done, circle, check, round, circular, confirm, approve]
category: Shapes
version: "1.0"
unicode: "ea67"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M9 12l2 2l4 -4" />
</svg>

After

Width:  |  Height:  |  Size: 429 B

+20
View File
@@ -0,0 +1,20 @@
<!--
tags: [cancel, "no", circle, round, circular, geometry, form, figure, pattern, outline]
category: Shapes
version: "1.0"
unicode: "ea6a"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M10 10l4 4m0 -4l-4 4" />
</svg>

After

Width:  |  Height:  |  Size: 441 B

@@ -0,0 +1,24 @@
<!--
tags: [copy, items, clipboard, list, file, paper, text, record, information]
category: Document
version: "1.0"
unicode: "ea6d"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2" />
<path d="M9 5a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2" />
<path d="M9 12l.01 0" />
<path d="M13 12l2 0" />
<path d="M9 16l.01 0" />
<path d="M13 16l2 0" />
</svg>

After

Width:  |  Height:  |  Size: 631 B

+20
View File
@@ -0,0 +1,20 @@
<!--
tags: [time, watch, alarm, clock, control, operation, function, interface, management]
category: System
version: "1.0"
unicode: "ea70"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path d="M12 7v5l3 3" />
</svg>

After

Width:  |  Height:  |  Size: 431 B

+21
View File
@@ -0,0 +1,21 @@
<!--
tags: [money, earn, salary, change, dollar, coin, store, purchase, shopping, retail]
category: E-commerce
version: "1.3"
unicode: "eb82"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M14.8 9a2 2 0 0 0 -1.8 -1h-2a2 2 0 1 0 0 4h2a2 2 0 1 1 0 4h-2a2 2 0 0 1 -1.8 -1" />
<path d="M12 7v10" />
</svg>

After

Width:  |  Height:  |  Size: 525 B

+22
View File
@@ -0,0 +1,22 @@
<!--
tags: [forbiddance, nixing, ban, interdicting, hand, stop, touch, action, motion, interaction]
category: Gestures
version: "1.10"
unicode: "ec2e"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 13v-7.5a1.5 1.5 0 0 1 3 0v6.5" />
<path d="M11 5.5v-2a1.5 1.5 0 1 1 3 0v8.5" />
<path d="M14 5.5a1.5 1.5 0 0 1 3 0v6.5" />
<path d="M17 7.5a1.5 1.5 0 0 1 3 0v8.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7a69.74 69.74 0 0 1 -.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47" />
</svg>

After

Width:  |  Height:  |  Size: 725 B

+21
View File
@@ -0,0 +1,21 @@
<!--
tags: [house, dashboard, living, building, home, main, architecture, structure, start, construction]
category: Buildings
version: "1.0"
unicode: "eac1"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12l-2 0l9 -9l9 9l-2 0" />
<path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7" />
<path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" />
</svg>

After

Width:  |  Height:  |  Size: 524 B

+20
View File
@@ -0,0 +1,20 @@
<!--
category: System
tags: [mail, gmail, email, envelope, post, inbox]
version: "1.0"
unicode: "eac4"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12" />
<path d="M4 13h3l3 3h4l3 -3h3" />
</svg>

After

Width:  |  Height:  |  Size: 447 B

+21
View File
@@ -0,0 +1,21 @@
<!--
tags: [information, advice, news, tip, sign, info, circle, control, operation, round]
category: System
version: "1.0"
unicode: "eac5"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path d="M12 9h.01" />
<path d="M11 12h1v4h1" />
</svg>

After

Width:  |  Height:  |  Size: 456 B

+21
View File
@@ -0,0 +1,21 @@
<!--
tags: [security, password, secure, lock, private, control, operation, protected, closed, function]
category: System
version: "1.0"
unicode: "eae2"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6" />
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0" />
<path d="M8 11v-4a4 4 0 1 1 8 0v4" />
</svg>

After

Width:  |  Height:  |  Size: 548 B

+21
View File
@@ -0,0 +1,21 @@
<!--
tags: [exit, shut, unplug, close, logout, control, operation, function, interface, management]
category: System
version: "1.4"
unicode: "eba8"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2" />
<path d="M9 12h12l-3 -3" />
<path d="M18 15l3 -3" />
</svg>

After

Width:  |  Height:  |  Size: 512 B

+20
View File
@@ -0,0 +1,20 @@
<!--
tags: [navigation, location, travel, pin, position, marker, map, attach, fix, mark]
category: Map
version: "1.0"
unicode: "eae8"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 11a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
<path d="M17.657 16.657l-4.243 4.243a2 2 0 0 1 -2.827 0l-4.244 -4.243a8 8 0 1 1 11.314 0" />
</svg>

After

Width:  |  Height:  |  Size: 491 B

@@ -0,0 +1,19 @@
<!--
tags: [comment, chat, reply, message, circle, contact, conversation, round, circular, messaging]
category: Communication
version: "1.0"
unicode: "eaed"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 20l1.3 -3.9c-2.324 -3.437 -1.426 -7.872 2.1 -10.374c3.526 -2.501 8.59 -2.296 11.845 .48c3.255 2.777 3.695 7.266 1.029 10.501c-2.666 3.235 -7.615 4.215 -11.574 2.293l-4.7 1" />
</svg>

After

Width:  |  Height:  |  Size: 559 B

@@ -0,0 +1,21 @@
<!--
tags: [face, emoji, emotion, mood, neutral]
category: Mood
version: "1.0"
unicode: "eaf5"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M9 10l.01 0" />
<path d="M15 10l.01 0" />
</svg>

After

Width:  |  Height:  |  Size: 414 B

+27
View File
@@ -0,0 +1,27 @@
<!--
tags: [technology, ai, machine, bot, android, robot, entertainment, playing, recreation, fun]
category: Games
version: "1.53"
unicode: "f00b"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M6 6a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2l0 -4" />
<path d="M12 2v2" />
<path d="M9 12v9" />
<path d="M15 12v9" />
<path d="M5 16l4 -2" />
<path d="M15 14l4 2" />
<path d="M9 18h6" />
<path d="M10 8v.01" />
<path d="M14 8v.01" />
</svg>

After

Width:  |  Height:  |  Size: 646 B

+20
View File
@@ -0,0 +1,20 @@
<!--
category: System
tags: [find, magnifier, magnifying glass, search, look, seek, query, browse]
version: "1.0"
unicode: "eb1c"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
<path d="M21 21l-6 -6" />
</svg>

After

Width:  |  Height:  |  Size: 422 B

+19
View File
@@ -0,0 +1,19 @@
<!--
tags: [safety, protect, protection, shield, control, operation, function, interface, management]
category: System
version: "1.0"
unicode: "eb24"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3" />
</svg>

After

Width:  |  Height:  |  Size: 461 B

+19
View File
@@ -0,0 +1,19 @@
<!--
category: System
tags: [star, light, fire, shine, sparkles]
version: "2.1"
unicode: "f6d7"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2m0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2m-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6" />
</svg>

After

Width:  |  Height:  |  Size: 522 B

+19
View File
@@ -0,0 +1,19 @@
<!--
tags: [favorite, like, mark, bookmark, grade, star, control, operation, rating, function]
category: System
version: "1.0"
unicode: "eb2e"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873l-6.158 -3.245" />
</svg>

After

Width:  |  Height:  |  Size: 489 B

+22
View File
@@ -0,0 +1,22 @@
<!--
tags: [timer, time, watch, clock, run, race, stopwatch, control, operation, function]
category: System
version: "3.12"
unicode: "ff9b"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 13a7 7 0 1 0 14 0a7 7 0 0 0 -14 0" />
<path d="M14.5 10.5l-2.5 2.5" />
<path d="M17 8l1 -1" />
<path d="M14 3h-4" />
</svg>

After

Width:  |  Height:  |  Size: 489 B

+19
View File
@@ -0,0 +1,19 @@
<!--
tags: [dislike, bad, emotion, thumb, down, bottom, decrease, control, operation, fall]
category: System
version: "1.0"
unicode: "eb3b"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M7 13v-8a1 1 0 0 0 -1 -1h-2a1 1 0 0 0 -1 1v7a1 1 0 0 0 1 1h3a4 4 0 0 1 4 4v1a2 2 0 0 0 4 0v-5h3a2 2 0 0 0 2 -2l-1 -5a2 3 0 0 0 -2 -2h-7a3 3 0 0 0 -3 3" />
</svg>

After

Width:  |  Height:  |  Size: 518 B

+19
View File
@@ -0,0 +1,19 @@
<!--
tags: [like, emotion, good, love, thumb, top, increase, control, operation, rise]
category: System
version: "1.0"
unicode: "eb3c"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M7 11v8a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-7a1 1 0 0 1 1 -1h3a4 4 0 0 0 4 -4v-1a2 2 0 0 1 4 0v5h3a2 2 0 0 1 2 2l-1 5a2 3 0 0 1 -2 2h-7a3 3 0 0 1 -3 -3" />
</svg>

After

Width:  |  Height:  |  Size: 513 B

+24
View File
@@ -0,0 +1,24 @@
<!--
category: Sport
tags: [success, win, prize, winner, trophy]
version: "1.0"
unicode: "eb45"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 21l8 0" />
<path d="M12 17l0 4" />
<path d="M7 4l10 0" />
<path d="M17 4v8a5 5 0 0 1 -10 0v-8" />
<path d="M3 9a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M17 9a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
</svg>

After

Width:  |  Height:  |  Size: 525 B

+20
View File
@@ -0,0 +1,20 @@
<!--
tags: [person, account, user, control, operation, profile, member, function, interface, management]
category: System
version: "1.0"
unicode: "eb4d"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" />
<path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
</svg>

After

Width:  |  Height:  |  Size: 471 B

+22
View File
@@ -0,0 +1,22 @@
<!--
tags: [people, persons, accounts, users, control, operation, function, interface, management]
category: System
version: "1.7"
unicode: "ebf2"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 7a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" />
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg>

After

Width:  |  Height:  |  Size: 550 B

+23
View File
@@ -0,0 +1,23 @@
<!--
tags: [earth, globe, global, language, union, world, location, navigation, geography, place]
category: Map
version: "1.0"
unicode: "eb54"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path d="M3.6 9h16.8" />
<path d="M3.6 15h16.8" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>

After

Width:  |  Height:  |  Size: 542 B

+15 -14
View File
@@ -2,6 +2,7 @@
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import AppIcon from '@/components/ui/AppIcon.vue'
const auth = useAuthStore()
const route = useRoute()
@@ -9,22 +10,22 @@ const route = useRoute()
const navItems = computed(() => {
const role = auth.user?.role ?? 'student'
if (role === 'teacher') return [
{ label: 'Дашборд', icon: '📊', to: '/teacher' },
{ label: 'Лекции', icon: '📖', to: '/teacher/lectures' },
{ label: 'Аналитика',icon: '📈', to: '/teacher/analytics' },
{ label: 'Профиль', icon: '👤', to: '/profile' },
{ label: 'Дашборд', icon: 'chart-bar', to: '/teacher' },
{ label: 'Лекции', icon: 'book-2', to: '/teacher/lectures' },
{ label: 'Аналитика', icon: 'chart-line', to: '/teacher/analytics' },
{ label: 'Профиль', icon: 'user', to: '/profile' },
]
if (role === 'admin') return [
{ label: 'Дашборд', icon: '🛡️', to: '/admin' },
{ label: 'Юзеры', icon: '👥', to: '/admin/users' },
{ label: 'Лекции', icon: '📚', to: '/admin/lectures' },
{ label: 'ИИ', icon: '🤖', to: '/admin/llm-queue' },
{ label: 'Дашборд', icon: 'shield', to: '/admin' },
{ label: 'Юзеры', icon: 'users', to: '/admin/users' },
{ label: 'Лекции', icon: 'books', to: '/admin/lectures' },
{ label: 'ИИ', icon: 'robot', to: '/admin/llm-queue' },
]
return [
{ label: 'Главная', icon: '🏠', to: '/' },
{ label: 'Лекции', icon: '📚', to: '/catalog' },
{ label: 'Мои', icon: '📋', to: '/my-lectures' },
{ label: 'Профиль', icon: '👤', to: '/profile' },
{ label: 'Главная', icon: 'home', to: '/' },
{ label: 'Лекции', icon: 'books', to: '/catalog' },
{ label: 'Мои', icon: 'clipboard-list', to: '/my-lectures' },
{ label: 'Профиль', icon: 'user', to: '/profile' },
]
})
@@ -43,7 +44,7 @@ function isActive(to: string) {
class="bottom-nav-item"
:class="{ active: isActive(item.to) }"
>
<span class="bottom-nav-icon">{{ item.icon }}</span>
<AppIcon class="bottom-nav-icon" :icon="item.icon" :size="20" />
<span class="bottom-nav-label">{{ item.label }}</span>
</RouterLink>
</nav>
@@ -78,7 +79,7 @@ function isActive(to: string) {
transition: color 0.2s;
}
.bottom-nav-item.active { color: var(--color-primary-dark); }
.bottom-nav-icon { font-size: 20px; }
.bottom-nav-icon { color: currentColor; }
.bottom-nav-label { font-size: 10px; font-weight: 600; }
@media (max-width: 768px) { .bottom-nav { display: flex; } }
+24 -21
View File
@@ -2,6 +2,7 @@
import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import AppIcon from '@/components/ui/AppIcon.vue'
const auth = useAuthStore()
const router = useRouter()
@@ -10,22 +11,22 @@ const route = useRoute()
interface NavItem { label: string; icon: string; to: string; roles: string[] }
const navItems: NavItem[] = [
{ label: 'Главная', icon: '🏠', to: '/', roles: ['student'] },
{ label: 'Каталог', icon: '📚', to: '/catalog', roles: ['student'] },
{ label: 'Мои лекции', icon: '📋', to: '/my-lectures', roles: ['student'] },
{ label: 'Достижения', icon: '🏆', to: '/achievements', roles: ['student'] },
{ label: 'Уведомления', icon: '🔔', to: '/notifications', roles: ['student'] },
{ label: 'Профиль', icon: '👤', to: '/profile', roles: ['student'] },
{ label: 'Главная', icon: 'home', to: '/', roles: ['student'] },
{ label: 'Каталог', icon: 'books', to: '/catalog', roles: ['student'] },
{ label: 'Мои лекции', icon: 'clipboard-list', to: '/my-lectures', roles: ['student'] },
{ label: 'Достижения', icon: 'trophy', to: '/achievements', roles: ['student'] },
{ label: 'Уведомления', icon: 'bell', to: '/notifications', roles: ['student'] },
{ label: 'Профиль', icon: 'user', to: '/profile', roles: ['student'] },
// Teacher
{ label: 'Дашборд', icon: '📊', to: '/teacher', roles: ['teacher'] },
{ label: 'Лекции', icon: '📖', to: '/teacher/lectures',roles: ['teacher'] },
{ label: 'Аналитика', icon: '📈', to: '/teacher/analytics',roles: ['teacher'] },
{ label: 'Профиль', icon: '👤', to: '/profile', roles: ['teacher'] },
{ label: 'Дашборд', icon: 'chart-bar', to: '/teacher', roles: ['teacher'] },
{ label: 'Лекции', icon: 'book-2', to: '/teacher/lectures', roles: ['teacher'] },
{ label: 'Аналитика', icon: 'chart-line', to: '/teacher/analytics', roles: ['teacher'] },
{ label: 'Профиль', icon: 'user', to: '/profile', roles: ['teacher'] },
// Admin
{ label: 'Дашборд', icon: '🛡️', to: '/admin', roles: ['admin'] },
{ label: 'Пользователи',icon: '👥', to: '/admin/users', roles: ['admin'] },
{ label: 'Лекции', icon: '📚', to: '/admin/lectures', roles: ['admin'] },
{ label: 'ИИ очередь', icon: '🤖', to: '/admin/llm-queue', roles: ['admin'] },
{ label: 'Дашборд', icon: 'shield', to: '/admin', roles: ['admin'] },
{ label: 'Пользователи', icon: 'users', to: '/admin/users', roles: ['admin'] },
{ label: 'Лекции', icon: 'books', to: '/admin/lectures', roles: ['admin'] },
{ label: 'ИИ очередь', icon: 'robot', to: '/admin/llm-queue', roles: ['admin'] },
]
const visible = computed(() =>
@@ -44,18 +45,19 @@ function isActive(to: string) {
<RouterLink
v-for="item in visible"
:key="item.to + item.label"
:to="item.to"
class="nav-item"
:class="{ active: isActive(item.to) }"
>
<span class="nav-icon">{{ item.icon }}</span>
:to="item.to"
class="nav-item"
:class="{ active: isActive(item.to) }"
>
<AppIcon class="nav-icon" :icon="item.icon" :size="17" />
<span class="nav-label">{{ item.label }}</span>
</RouterLink>
</nav>
<div class="sidebar-footer">
<button class="logout-btn" @click="auth.logout().then(() => router.push('/login'))">
🚪 Выйти
<AppIcon class="logout-icon" icon="logout" :size="16" />
Выйти
</button>
</div>
</aside>
@@ -107,7 +109,7 @@ function isActive(to: string) {
font-weight: 700;
box-shadow: 0 2px 8px rgba(34,197,94,0.12);
}
.nav-icon { font-size: 17px; flex-shrink: 0; }
.nav-icon { flex-shrink: 0; color: currentColor; }
.sidebar-footer { padding: 10px 18px 8px; }
.logout-btn {
width: 100%;
@@ -124,6 +126,7 @@ function isActive(to: string) {
align-items: center;
gap: 6px;
}
.logout-icon { color: currentColor; }
.logout-btn:hover { background: rgba(239,68,68,0.15); }
@media (max-width: 768px) { .sidebar { display: none; } }
+6 -5
View File
@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth'
import { useUserStore } from '@/stores/user'
import CoinChip from '@/components/ui/CoinChip.vue'
import SearchInput from '@/components/ui/SearchInput.vue'
import AppIcon from '@/components/ui/AppIcon.vue'
import { formatUserName } from '@/utils/formatUserName'
const auth = useAuthStore()
@@ -35,7 +36,7 @@ function openProfile() {
<template>
<header class="topbar">
<div class="topbar-brand">
<span class="brand-icon">🌍</span>
<AppIcon class="brand-icon" icon="world" :size="24" />
<span class="brand-name">UniVerse</span>
</div>
@@ -51,7 +52,7 @@ function openProfile() {
</button>
<button class="notif-btn" @click="$router.push('/notifications')">
🔔
<AppIcon icon="bell" :size="18" />
<span class="notif-dot" v-if="auth.user && unreadCount > 0">
{{ unreadCount }}
</span>
@@ -65,7 +66,7 @@ function openProfile() {
@keydown.enter.prevent="openProfile"
@keydown.space.prevent="openProfile"
>
<span class="avatar-icon">👤</span>
<AppIcon class="avatar-icon" icon="user" :size="18" />
<span class="avatar-name" v-if="auth.user">{{ formatUserName(auth.user.name) }}</span>
</div>
</div>
@@ -98,7 +99,7 @@ function openProfile() {
flex-shrink: 0;
cursor: pointer;
}
.brand-icon { font-size: 24px; }
.brand-icon { color: var(--color-text); }
.brand-name {
font-size: 20px;
font-weight: 800;
@@ -165,7 +166,7 @@ function openProfile() {
transition: all 0.2s;
}
.avatar:hover { background: rgba(255,255,255,0.8); }
.avatar-icon { font-size: 18px; }
.avatar-icon { color: var(--color-text-secondary); }
.avatar-name { font-size: 13px; font-weight: 600; color: var(--color-text); }
@media (max-width: 640px) {
@@ -1,4 +1,6 @@
<script setup lang="ts">
import AppIcon from '@/components/ui/AppIcon.vue'
defineProps<{
icon: string
title: string
@@ -11,15 +13,22 @@ defineProps<{
<template>
<div class="badge-card" :class="{ locked: !unlocked }">
<div class="badge-icon">{{ icon }}</div>
<AppIcon class="badge-icon" :icon="icon" :size="32" />
<div class="badge-body">
<div class="badge-title">{{ title }}</div>
<div class="badge-desc">{{ description }}</div>
<div class="badge-meta" v-if="unlocked && unlockedAt">
Получено {{ new Date(unlockedAt).toLocaleDateString('ru-RU') }}
<span v-if="coins" class="coins-tag">+{{ coins }} 💰</span>
<AppIcon class="meta-icon" icon="circle-check" :size="14" />
Получено {{ new Date(unlockedAt).toLocaleDateString('ru-RU') }}
<span v-if="coins" class="coins-tag">
+{{ coins }}
<AppIcon class="coin-icon" icon="coin" :size="14" />
</span>
</div>
<div class="badge-meta locked-msg" v-else-if="!unlocked">
<AppIcon class="meta-icon" icon="lock" :size="14" />
Заблокировано
</div>
<div class="badge-meta locked-msg" v-else-if="!unlocked">🔒 Заблокировано</div>
</div>
</div>
</template>
@@ -42,10 +51,11 @@ defineProps<{
opacity: 0.5;
filter: grayscale(0.5);
}
.badge-icon { font-size: 32px; line-height: 1; flex-shrink: 0; }
.badge-icon { flex-shrink: 0; color: var(--color-text); }
.badge-title { font-size: 15px; font-weight: 700; color: var(--color-text); margin-bottom: 3px; }
.badge-desc { font-size: 12px; color: var(--color-text-secondary); margin-bottom: 6px; }
.badge-meta { font-size: 11px; color: var(--color-text-secondary); }
.badge-meta { font-size: 11px; color: var(--color-text-secondary); display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.meta-icon { color: var(--color-text-secondary); }
.locked-msg { color: #9CA3AF; }
.coins-tag {
margin-left: 6px;
@@ -54,5 +64,9 @@ defineProps<{
padding: 1px 8px;
color: #92400E;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 4px;
}
.coin-icon { color: #92400E; }
</style>
+68
View File
@@ -0,0 +1,68 @@
<script setup lang="ts">
import { computed } from 'vue'
import { iconSvgs, normalizeIconName } from '@/icons'
const props = defineProps<{
icon?: string | null
size?: number | string
title?: string
}>()
const isUrl = computed(() => {
const value = props.icon ?? ''
return /^(https?:\/\/|\/|data:)/.test(value)
})
const svg = computed(() => {
const name = normalizeIconName(props.icon)
if (!name) return undefined
return iconSvgs[name]
})
const sizePx = computed(() => {
const size = props.size ?? 18
return typeof size === 'number' ? `${size}px` : size
})
</script>
<template>
<span
v-if="svg"
class="app-icon"
:style="{ '--size': sizePx }"
:aria-hidden="title ? undefined : 'true'"
:aria-label="title"
role="img"
v-html="svg"
/>
<img
v-else-if="isUrl && icon"
class="app-icon-img"
:src="icon"
:alt="title ?? ''"
:style="{ width: sizePx, height: sizePx }"
loading="lazy"
decoding="async"
/>
</template>
<style scoped>
.app-icon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.app-icon :deep(svg) {
display: block;
width: var(--size);
height: var(--size);
}
.app-icon-img {
display: inline-block;
vertical-align: middle;
}
</style>
+4 -2
View File
@@ -1,10 +1,12 @@
<script setup lang="ts">
import AppIcon from '@/components/ui/AppIcon.vue'
defineProps<{ amount: number }>()
</script>
<template>
<div class="coin-chip">
<span class="coin-icon">💰</span>
<AppIcon class="coin-icon" icon="coin" :size="16" />
<span class="coin-amount">{{ amount }}</span>
<span class="coin-label">монет</span>
</div>
@@ -26,7 +28,7 @@ defineProps<{ amount: number }>()
.coin-chip:hover {
background: linear-gradient(135deg, rgba(251,191,36,0.3), rgba(245,158,11,0.25));
}
.coin-icon { font-size: 16px; }
.coin-icon { color: #78350F; }
.coin-amount { font-weight: 800; font-size: 14px; color: #78350F; }
.coin-label { font-size: 12px; color: #92400E; }
</style>
+4 -2
View File
@@ -1,4 +1,6 @@
<script setup lang="ts">
import AppIcon from '@/components/ui/AppIcon.vue'
defineProps<{
icon?: string
title?: string
@@ -8,7 +10,7 @@ defineProps<{
<template>
<div class="empty-state">
<div class="empty-icon">{{ icon ?? '📭' }}</div>
<AppIcon class="empty-icon" :icon="icon ?? 'inbox'" :size="52" />
<div class="empty-title">{{ title ?? 'Ничего не найдено' }}</div>
<div class="empty-sub">{{ subtitle ?? 'Попробуйте изменить фильтры или вернитесь позже.' }}</div>
<slot />
@@ -26,7 +28,7 @@ defineProps<{
text-align: center;
color: var(--color-text-secondary);
}
.empty-icon { font-size: 52px; }
.empty-icon { color: var(--color-text-secondary); }
.empty-title { font-size: 18px; font-weight: 700; color: var(--color-text); }
.empty-sub { font-size: 14px; max-width: 320px; }
</style>
+25 -6
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { Lecture } from '@/types'
import { useRouter } from 'vue-router'
import AppIcon from '@/components/ui/AppIcon.vue'
const props = defineProps<{
lecture: Lecture
@@ -28,7 +29,8 @@ function goDetail() {
<div class="lecture-card" @click="goDetail">
<div class="card-header">
<span class="badge" :class="lecture.format === 'online' ? 'badge-blue' : 'badge-green'">
{{ lecture.format === 'online' ? '🌐 Онлайн' : '📍 Офлайн' }}
<AppIcon class="badge-icon" :icon="lecture.format === 'online' ? 'world' : 'map-pin'" :size="14" />
{{ lecture.format === 'online' ? 'Онлайн' : 'Офлайн' }}
</span>
<span
class="seats"
@@ -47,16 +49,28 @@ function goDetail() {
<h3 class="card-title">{{ lecture.title }}</h3>
<div class="card-teacher">
<span>👤</span>
<AppIcon class="teacher-icon" icon="user" :size="16" />
<span>{{ lecture.teacher }}</span>
<span class="text-secondary">· {{ lecture.institute }}</span>
</div>
<div class="card-meta">
<span>📅 {{ formatDate(lecture.date) }}</span>
<span> {{ lecture.time }}</span>
<span v-if="lecture.room">🏛 {{ lecture.building }}, ауд. {{ lecture.room }}</span>
<span v-else>🏛 {{ lecture.building }}</span>
<span class="meta-item">
<AppIcon class="meta-icon" icon="calendar" :size="14" />
{{ formatDate(lecture.date) }}
</span>
<span class="meta-item">
<AppIcon class="meta-icon" icon="clock" :size="14" />
{{ lecture.time }}
</span>
<span v-if="lecture.room" class="meta-item">
<AppIcon class="meta-icon" icon="building" :size="14" />
{{ lecture.building }}, ауд. {{ lecture.room }}
</span>
<span v-else class="meta-item">
<AppIcon class="meta-icon" icon="building" :size="14" />
{{ lecture.building }}
</span>
</div>
<div class="card-tags">
@@ -117,6 +131,8 @@ function goDetail() {
justify-content: space-between;
gap: 8px;
}
.badge { display: inline-flex; align-items: center; gap: 4px; }
.badge-icon { color: currentColor; }
.seats {
font-size: 12px;
font-weight: 600;
@@ -141,6 +157,7 @@ function goDetail() {
font-size: 13px;
color: var(--color-text-secondary);
}
.teacher-icon { color: var(--color-text-secondary); }
.card-meta {
display: flex;
flex-wrap: wrap;
@@ -148,6 +165,8 @@ function goDetail() {
font-size: 12px;
color: var(--color-text-secondary);
}
.meta-item { display: inline-flex; align-items: center; gap: 4px; }
.meta-icon { color: var(--color-text-secondary); }
.card-tags {
display: flex;
flex-wrap: wrap;
+4 -2
View File
@@ -1,4 +1,6 @@
<script setup lang="ts">
import AppIcon from '@/components/ui/AppIcon.vue'
const props = defineProps<{
modelValue: string
placeholder?: string
@@ -8,7 +10,7 @@ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
<template>
<div class="search-wrap">
<span class="search-icon">🔍</span>
<AppIcon class="search-icon" icon="search" :size="15" />
<input
class="search-input"
type="text"
@@ -28,7 +30,7 @@ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
.search-icon {
position: absolute;
left: 12px;
font-size: 15px;
color: var(--color-text-secondary);
pointer-events: none;
}
.search-input {
+4 -3
View File
@@ -1,4 +1,6 @@
<script setup lang="ts">
import AppIcon from '@/components/ui/AppIcon.vue'
defineProps<{
label: string
value: string | number
@@ -10,7 +12,7 @@ defineProps<{
<template>
<div class="stats-widget" :class="`color-${color ?? 'green'}`">
<div class="widget-icon" v-if="icon">{{ icon }}</div>
<AppIcon v-if="icon" class="widget-icon" :icon="icon" :size="28" />
<div class="widget-body">
<div class="widget-value">{{ value }}</div>
<div class="widget-label">{{ label }}</div>
@@ -47,9 +49,8 @@ defineProps<{
.color-purple::after { background: linear-gradient(90deg, #A78BFA, #C4B5FD); }
.widget-icon {
font-size: 28px;
line-height: 1;
flex-shrink: 0;
color: var(--color-text);
}
.widget-body { flex: 1; min-width: 0; }
.widget-value {
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import AppIcon from '@/components/ui/AppIcon.vue'
const props = defineProps<{
message: string
@@ -14,13 +15,13 @@ onMounted(() => {
setTimeout(() => { visible.value = false; setTimeout(() => emit('close'), 350) }, props.duration ?? 3000)
})
const iconMap = { success: '', error: '', info: '' }
const iconNameMap = { success: 'circle-check', error: 'circle-x', info: 'info-circle' } as const
</script>
<template>
<Transition name="toast">
<div v-if="visible" class="toast" :class="type ?? 'success'">
<span>{{ iconMap[type ?? 'success'] }}</span>
<AppIcon :icon="iconNameMap[type ?? 'success']" :size="18" />
<span>{{ message }}</span>
<button class="toast-close" @click="emit('close')">×</button>
</div>
+127
View File
@@ -0,0 +1,127 @@
import alarm from '@/assets/icons/alarm.svg?raw'
import alertTriangle from '@/assets/icons/alert-triangle.svg?raw'
import bell from '@/assets/icons/bell.svg?raw'
import book2 from '@/assets/icons/book-2.svg?raw'
import books from '@/assets/icons/books.svg?raw'
import building from '@/assets/icons/building.svg?raw'
import bulb from '@/assets/icons/bulb.svg?raw'
import calendar from '@/assets/icons/calendar.svg?raw'
import calendarEvent from '@/assets/icons/calendar-event.svg?raw'
import chartBar from '@/assets/icons/chart-bar.svg?raw'
import chartLine from '@/assets/icons/chart-line.svg?raw'
import circleCheck from '@/assets/icons/circle-check.svg?raw'
import circleX from '@/assets/icons/circle-x.svg?raw'
import clipboardList from '@/assets/icons/clipboard-list.svg?raw'
import clock from '@/assets/icons/clock.svg?raw'
import coin from '@/assets/icons/coin.svg?raw'
import handStop from '@/assets/icons/hand-stop.svg?raw'
import home from '@/assets/icons/home.svg?raw'
import inbox from '@/assets/icons/inbox.svg?raw'
import infoCircle from '@/assets/icons/info-circle.svg?raw'
import lock from '@/assets/icons/lock.svg?raw'
import logout from '@/assets/icons/logout.svg?raw'
import mapPin from '@/assets/icons/map-pin.svg?raw'
import messageCircle from '@/assets/icons/message-circle.svg?raw'
import moodNeutral from '@/assets/icons/mood-neutral.svg?raw'
import robot from '@/assets/icons/robot.svg?raw'
import search from '@/assets/icons/search.svg?raw'
import shield from '@/assets/icons/shield.svg?raw'
import sparkles from '@/assets/icons/sparkles.svg?raw'
import star from '@/assets/icons/star.svg?raw'
import stopwatch from '@/assets/icons/stopwatch.svg?raw'
import thumbDown from '@/assets/icons/thumb-down.svg?raw'
import thumbUp from '@/assets/icons/thumb-up.svg?raw'
import trophy from '@/assets/icons/trophy.svg?raw'
import user from '@/assets/icons/user.svg?raw'
import users from '@/assets/icons/users.svg?raw'
import world from '@/assets/icons/world.svg?raw'
export const iconSvgs = {
alarm,
'alert-triangle': alertTriangle,
bell,
'book-2': book2,
books,
building,
bulb,
calendar,
'calendar-event': calendarEvent,
'chart-bar': chartBar,
'chart-line': chartLine,
'circle-check': circleCheck,
'circle-x': circleX,
'clipboard-list': clipboardList,
clock,
coin,
'hand-stop': handStop,
home,
inbox,
'info-circle': infoCircle,
lock,
logout,
'map-pin': mapPin,
'message-circle': messageCircle,
'mood-neutral': moodNeutral,
robot,
search,
shield,
sparkles,
star,
stopwatch,
'thumb-down': thumbDown,
'thumb-up': thumbUp,
trophy,
user,
users,
world,
} as const
export type IconName = keyof typeof iconSvgs
export const emojiToIcon: Record<string, IconName> = {
'⏰': 'alarm',
'⚠️': 'alert-triangle',
'🔔': 'bell',
'📖': 'book-2',
'📚': 'books',
'🏛': 'building',
'💡': 'bulb',
'📅': 'calendar',
'🗓️': 'calendar-event',
'📊': 'chart-bar',
'📈': 'chart-line',
'✅': 'circle-check',
'❌': 'circle-x',
'📋': 'clipboard-list',
'⏱': 'stopwatch',
'⏱️': 'stopwatch',
'🕒': 'clock',
'💰': 'coin',
'👋': 'hand-stop',
'🏠': 'home',
'📭': 'inbox',
'️': 'info-circle',
'🔒': 'lock',
'🚪': 'logout',
'📍': 'map-pin',
'💬': 'message-circle',
'😐': 'mood-neutral',
'🤖': 'robot',
'🔍': 'search',
'🛡️': 'shield',
'✨': 'sparkles',
'⭐': 'star',
'👍': 'thumb-up',
'👎': 'thumb-down',
'🏆': 'trophy',
'👤': 'user',
'👥': 'users',
'🌍': 'world',
'🌐': 'world',
} as const
export function normalizeIconName(input?: string | null): IconName | undefined {
if (!input) return undefined
if (input in iconSvgs) return input as IconName
return emojiToIcon[input]
}
@@ -44,10 +44,10 @@ onMounted(async () => {
<h1 class="page-title">Дашборд администратора</h1>
<div class="stats-row">
<StatsWidget label="Пользователей" :value="users.length" icon="👥" color="green" />
<StatsWidget label="Лекций" :value="lectures.length" icon="📚" color="aqua" />
<StatsWidget label="Записей" :value="enrollmentCount" icon="🗓️" color="orange" />
<StatsWidget label="Отзывов в LLM" :value="reviews.length" icon="💬" color="purple" />
<StatsWidget label="Пользователей" :value="users.length" icon="users" color="green" />
<StatsWidget label="Лекций" :value="lectures.length" icon="books" color="aqua" />
<StatsWidget label="Записей" :value="enrollmentCount" icon="calendar-event" color="orange" />
<StatsWidget label="Отзывов в LLM" :value="reviews.length" icon="message-circle" color="purple" />
</div>
<div class="grid">
+13 -3
View File
@@ -2,6 +2,7 @@
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import AppIcon from '@/components/ui/AppIcon.vue'
const auth = useAuthStore()
const route = useRoute()
@@ -23,7 +24,9 @@ async function login() {
<div class="login-bg">
<div class="login-card">
<div class="login-header">
<div class="logo-mark">🌍</div>
<div class="logo-mark">
<AppIcon icon="world" :size="52" />
</div>
<h1 class="brand">UniVerse</h1>
<p class="brand-sub">«Откройте для себя вселенную знаний»</p>
</div>
@@ -40,7 +43,10 @@ async function login() {
</span>
{{ loading ? 'Вход...' : 'Войти через ЮФУ (Microsoft Entra ID)' }}
</button>
<div class="error" v-if="error"> {{ error }}</div>
<div class="error" v-if="error">
<AppIcon class="error-icon" icon="alert-triangle" :size="16" />
{{ error }}
</div>
</div>
<div class="login-footer">
@@ -73,7 +79,7 @@ async function login() {
gap: 20px;
}
.login-header { text-align: center; }
.logo-mark { font-size: 52px; }
.logo-mark { display: inline-flex; justify-content: center; color: var(--color-text); }
.brand {
font-size: 34px;
font-weight: 900;
@@ -102,7 +108,11 @@ async function login() {
border: 1px solid rgba(239,68,68,0.3);
border-radius: var(--radius-sm);
padding: 8px 12px;
display: flex;
align-items: center;
gap: 8px;
}
.error-icon { color: var(--color-error); }
.login-footer {
text-align: center;
font-size: 12px;
+44 -11
View File
@@ -10,6 +10,7 @@ import LectureCard from '@/components/ui/LectureCard.vue'
import ProgressBar from '@/components/ui/ProgressBar.vue'
import AchievementBadge from '@/components/ui/AchievementBadge.vue'
import EmptyState from '@/components/ui/EmptyState.vue'
import AppIcon from '@/components/ui/AppIcon.vue'
import { formatUserName } from '@/utils/formatUserName'
const auth = useAuthStore()
@@ -40,7 +41,10 @@ onMounted(async () => {
<div class="dashboard page-content">
<div class="dashboard-welcome">
<div>
<h1 class="page-title">Добрый день, {{ formatUserName(user.name) }}! 👋</h1>
<h1 class="page-title">
Добрый день, {{ formatUserName(user.name) }}!
<AppIcon class="inline-icon" icon="hand-stop" :size="18" />
</h1>
<p class="text-secondary">{{ user.institute }} · {{ user.direction }} · {{ user.year }} курс</p>
</div>
<div class="quick-actions">
@@ -56,9 +60,18 @@ onMounted(async () => {
<div class="section-title">Ближайшая лекция</div>
<div class="next-title">{{ nextLecture.title }}</div>
<div class="next-meta">
<span>📅 Завтра, {{ nextLecture.time }}</span>
<span>🏛 {{ nextLecture.building }}, ауд. {{ nextLecture.room ?? 'онлайн' }}</span>
<span>👤 {{ nextLecture.teacher }}</span>
<span class="meta-line">
<AppIcon class="meta-icon" icon="calendar" :size="14" />
Завтра, {{ nextLecture.time }}
</span>
<span class="meta-line">
<AppIcon class="meta-icon" icon="building" :size="14" />
{{ nextLecture.building }}, ауд. {{ nextLecture.room ?? 'онлайн' }}
</span>
<span class="meta-line">
<AppIcon class="meta-icon" icon="user" :size="14" />
{{ nextLecture.teacher }}
</span>
</div>
</div>
<div class="next-actions">
@@ -70,10 +83,10 @@ onMounted(async () => {
<EmptyState v-else-if="!lectures.loading" title="Пока нет лекций" subtitle="Каталог пуст или данные ещё не синхронизированы." />
<div class="stats-row">
<StatsWidget label="Посещено лекций" :value="user.lecturesAttended ?? 12" icon="📚" color="green" />
<StatsWidget label="Часов обучения" :value="user.hoursLearned ?? 18.5" icon="" color="aqua" />
<StatsWidget label="Монет" :value="user.coins" icon="💰" color="orange" />
<StatsWidget label="Уровень" :value="user.level" icon="" color="purple" sub="текущий уровень" />
<StatsWidget label="Посещено лекций" :value="user.lecturesAttended ?? 12" icon="books" color="green" />
<StatsWidget label="Часов обучения" :value="user.hoursLearned ?? 18.5" icon="stopwatch" color="aqua" />
<StatsWidget label="Монет" :value="user.coins" icon="coin" color="orange" />
<StatsWidget label="Уровень" :value="user.level" icon="star" color="purple" sub="текущий уровень" />
</div>
<GlassCard>
@@ -88,7 +101,12 @@ onMounted(async () => {
<section>
<div class="section-header">
<h2 class="section-title"> Рекомендуемые лекции</h2>
<h2 class="section-title">
<span class="title-with-icon">
<AppIcon class="title-icon" icon="sparkles" :size="18" />
Рекомендуемые лекции
</span>
</h2>
<button class="link-btn" @click="router.push('/catalog')">Все лекции </button>
</div>
<EmptyState v-if="lectures.loading" title="Загружаем рекомендации" subtitle="Получаем данные с backend." />
@@ -105,7 +123,12 @@ onMounted(async () => {
<section class="two-column">
<GlassCard>
<div class="section-title">🏆 Достижения</div>
<div class="section-title">
<span class="title-with-icon">
<AppIcon class="title-icon" icon="trophy" :size="18" />
Достижения
</span>
</div>
<div class="achievements">
<AchievementBadge
v-for="a in achievements"
@@ -120,7 +143,12 @@ onMounted(async () => {
</div>
</GlassCard>
<GlassCard>
<div class="section-title">🔔 Напоминания</div>
<div class="section-title">
<span class="title-with-icon">
<AppIcon class="title-icon" icon="bell" :size="18" />
Напоминания
</span>
</div>
<div class="reminders">
<div class="reminder-item" v-for="n in reminders" :key="n.id">
<div class="reminder-title">{{ n.title }}</div>
@@ -146,6 +174,8 @@ onMounted(async () => {
.next-lecture { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.next-title { font-size: 18px; font-weight: 700; margin: 6px 0; }
.next-meta { display: flex; flex-direction: column; gap: 4px; color: var(--color-text-secondary); font-size: 13px; }
.meta-line { display: inline-flex; align-items: center; gap: 6px; }
.meta-icon { color: var(--color-text-secondary); }
.next-actions { display: flex; gap: 10px; align-items: flex-start; }
.stats-row {
display: grid;
@@ -156,6 +186,9 @@ onMounted(async () => {
.xp-header { display: flex; justify-content: space-between; font-size: 13px; font-weight: 600; }
.xp-val { color: var(--color-text-secondary); }
.section-header { display: flex; align-items: center; justify-content: space-between; }
.title-with-icon { display: inline-flex; align-items: center; gap: 8px; }
.title-icon { color: var(--color-text); }
.inline-icon { color: var(--color-text); vertical-align: middle; }
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
@@ -2,6 +2,7 @@
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'
import GlassCard from '@/components/ui/GlassCard.vue'
import AppIcon from '@/components/ui/AppIcon.vue'
const userStore = useUserStore()
@@ -16,11 +17,11 @@ const grouped = computed(() => {
})
const typeIcon: Record<string, string> = {
reminder: '',
'schedule-change': '🗓️',
achievement: '🏆',
coins: '💰',
recommendation: '',
reminder: 'alarm',
'schedule-change': 'calendar-event',
achievement: 'trophy',
coins: 'coin',
recommendation: 'sparkles',
}
</script>
@@ -36,7 +37,7 @@ const typeIcon: Record<string, string> = {
<div class="group-title">{{ day }}</div>
<div class="items">
<div v-for="n in items" :key="n.id" class="item" :class="{ unread: !n.read }">
<div class="icon">{{ typeIcon[n.type] }}</div>
<AppIcon class="icon" :icon="typeIcon[n.type]" :size="20" />
<div>
<div class="item-title">{{ n.title }}</div>
<div class="item-body">{{ n.body }}</div>
@@ -56,7 +57,7 @@ const typeIcon: Record<string, string> = {
.items { display: flex; flex-direction: column; gap: 10px; }
.item { display: flex; gap: 12px; padding: 10px; border-radius: var(--radius-sm); background: rgba(255,255,255,0.6); border: 1px solid var(--color-border-glass); }
.item.unread { border-color: rgba(34,197,94,0.4); background: rgba(34,197,94,0.08); }
.icon { font-size: 20px; }
.icon { color: var(--color-text); flex-shrink: 0; }
.item-title { font-weight: 600; }
.item-body { font-size: 13px; color: var(--color-text-secondary); }
</style>