update
This commit is contained in:
@@ -1,43 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1072.73 329.6" width="111" height="36" title="EDUSEG">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path
|
|
||||||
fill="#8cd366"
|
|
||||||
d="M152.18,217.62l-68.61-30.91c-1.88-1.2-4.28-1.2-6.16,0l-68.61,30.91c-3.81,2.43-8.8-.3-8.8-4.82V8.98C0,5.82,2.56,3.26,5.72,3.26h149.54c3.16,0,5.72,2.56,5.72,5.72v203.81c0,4.52-5,7.26-8.8,4.82Z"
|
|
||||||
></path>
|
|
||||||
<path fill="#2e3524" d="M93.97,74.01H26.61v20.16h67.36v-20.16Z"></path>
|
|
||||||
<path fill="#2e3524" d="M107.44,111.16H26.61v26.8h80.83v-26.8Z"></path>
|
|
||||||
<path fill="#2e3524" d="M107.44,30.27H26.61v26.8h80.83v-26.8Z"></path>
|
|
||||||
<path
|
|
||||||
fill="#2e3524"
|
|
||||||
d="M134.38,131.23c0-3.72-3.02-6.73-6.73-6.73s-6.73,3.02-6.73,6.73,3.02,6.73,6.73,6.73,6.73-3.02,6.73-6.73Z"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path fill="#f9f7e8" d="M244.7,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#f9f7e8"
|
|
||||||
d="M362.72,3.24h57.79c10.71,0,20.47,2.35,29.29,7.06,8.83,4.7,15.79,11.18,20.87,19.39,5.08,8.21,7.63,17.45,7.63,27.67v214.88c0,10.22-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.83,4.7-18.73,7.08-29.7,7.08h-57.79V3.24ZM427.55,283.88c1.74-1.87,2.6-4.18,2.6-6.86V52.56c-.26-2.69-1.34-4.97-3.22-6.86-1.88-1.87-4.15-2.83-6.82-2.83h-14v243.85h14.41c2.93,0,5.27-.94,7.01-2.83h.02Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#f9f7e8"
|
|
||||||
d="M531.5,322.49c-8.71-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67V3.24h48.15v279.41c0,2.69.93,4.99,2.82,6.86,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.86V3.24h48.16v272.21c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.7-18.73,7.08-29.7,7.08s-20.8-2.35-29.5-7.08l.05-.02Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#f9f7e8"
|
|
||||||
d="M672.79,322.49c-8.7-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67v-78.75h48.16v85.95c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-77.88c0-5.66-2.22-10.3-6.63-13.94-4.41-3.62-11.57-8.02-21.47-13.13-8.3-4.3-15.05-8.14-20.27-11.52-5.22-3.36-9.71-7.87-13.45-13.54-3.75-5.66-5.63-12.24-5.63-19.8V54.12c0-10.22,2.53-19.44,7.63-27.67,5.08-8.21,11.97-14.66,20.68-19.39,8.68-4.7,18.53-7.06,29.5-7.06s20.87,2.35,29.69,7.06c8.83,4.7,15.72,11.18,20.68,19.39,4.96,8.21,7.42,17.45,7.42,27.67v71.09h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v69.79c0,6.19,2.34,11.26,7.04,15.14,4.67,3.91,12.24,8.83,22.68,14.74,8.04,4.32,14.57,8.09,19.68,11.3,5.08,3.24,9.37,7.46,12.83,12.72,3.48,5.26,5.22,11.26,5.22,17.98v86.83c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.71-18.72,7.08-29.7,7.08s-20.8-2.35-29.5-7.08Z"
|
|
||||||
></path>
|
|
||||||
<path fill="#f9f7e8" d="M784.56,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#f9f7e8"
|
|
||||||
d="M920.63,322.49c-5.63-4.18-10.11-10.1-13.45-17.76-3.34-7.68-5.01-16.49-5.01-26.45V53.71c0-9.96,2.53-19.06,7.63-27.26,5.08-8.21,12.05-14.66,20.87-19.39,8.83-4.7,18.6-7.06,29.32-7.06s20.54,2.35,29.5,7.06c8.97,4.7,15.91,11.18,20.87,19.39,4.96,8.21,7.42,17.3,7.42,27.26v94.51h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v231.36c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-46.03h-11.64v-51.29h59.8v145.4h-48.16v-14.14c-2.96,5.4-6.82,9.48-11.64,12.31-4.82,2.83-10.83,4.25-18.06,4.25s-13.64-2.09-19.27-6.26l-.02-.02Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#f9f7e8"
|
|
||||||
d="M1053.27,25.05h-6.13l-.06-3.69h5.48c.83-.02,1.61-.15,2.33-.4.72-.27,1.3-.64,1.73-1.14.44-.51.65-1.14.65-1.87,0-.93-.16-1.67-.48-2.22-.3-.55-.83-.94-1.59-1.16-.74-.25-1.74-.37-3.01-.37h-3.78v20.42h-4.12V10.54h7.9c1.87,0,3.49.27,4.86.82,1.38.53,2.44,1.34,3.18,2.44.76,1.08,1.14,2.43,1.14,4.06,0,1.02-.24,1.93-.71,2.73-.47.8-1.17,1.49-2.1,2.07-.91.57-2.03,1.03-3.35,1.39-.06,0-.12.07-.2.2-.06.13-.11.2-.17.2-.32.19-.53.33-.63.43-.08.08-.16.12-.25.14-.08.02-.31.03-.68.03ZM1052.99,25.05l.6-2.81c2.95,0,4.97.64,6.05,1.93,1.08,1.27,1.62,2.89,1.62,4.86v1.53c0,.7.03,1.37.08,2.02.08.62.21,1.15.4,1.59v.45h-4.23c-.19-.49-.3-1.19-.34-2.1-.02-.91-.03-1.57-.03-1.99v-1.48c0-1.38-.31-2.39-.94-3.04s-1.69-.97-3.21-.97ZM1035.75,22.92c0,2.52.43,4.87,1.28,7.04.87,2.16,2.08,4.05,3.64,5.68,1.55,1.61,3.34,2.87,5.37,3.78,2.05.89,4.22,1.33,6.53,1.33s4.51-.44,6.53-1.33c2.03-.91,3.8-2.17,5.34-3.78,1.53-1.63,2.74-3.52,3.61-5.68.87-2.18,1.31-4.52,1.31-7.04s-.44-4.86-1.31-7.01c-.87-2.16-2.07-4.04-3.61-5.65-1.53-1.61-3.31-2.86-5.34-3.75-2.03-.91-4.2-1.36-6.53-1.36s-4.49.45-6.53,1.36c-2.03.89-3.81,2.14-5.37,3.75-1.55,1.61-2.77,3.49-3.64,5.65-.85,2.16-1.28,4.5-1.28,7.01ZM1032.4,22.92c0-3.01.52-5.8,1.56-8.38,1.04-2.57,2.49-4.82,4.34-6.73,1.86-1.93,4-3.43,6.42-4.49,2.44-1.08,5.06-1.62,7.84-1.62s5.39.54,7.81,1.62c2.44,1.06,4.58,2.56,6.42,4.49,1.86,1.91,3.31,4.16,4.35,6.73,1.06,2.57,1.59,5.37,1.59,8.38s-.53,5.8-1.59,8.38c-1.04,2.57-2.49,4.84-4.35,6.79-1.83,1.93-3.97,3.44-6.42,4.52-2.42,1.08-5.03,1.62-7.81,1.62s-5.39-.54-7.84-1.62c-2.42-1.08-4.56-2.58-6.42-4.52-1.85-1.95-3.3-4.21-4.34-6.79-1.04-2.57-1.56-5.37-1.56-8.38Z"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,43 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1072.73 329.6" width="111" height="36" title="EDUSEG">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path
|
|
||||||
fill="#8cd366"
|
|
||||||
d="M152.18,217.62l-68.61-30.91c-1.88-1.2-4.28-1.2-6.16,0l-68.61,30.91c-3.81,2.43-8.8-.3-8.8-4.82V8.98C0,5.82,2.56,3.26,5.72,3.26h149.54c3.16,0,5.72,2.56,5.72,5.72v203.81c0,4.52-5,7.26-8.8,4.82Z"
|
|
||||||
></path>
|
|
||||||
<path fill="#2e3524" d="M93.97,74.01H26.61v20.16h67.36v-20.16Z"></path>
|
|
||||||
<path fill="#2e3524" d="M107.44,111.16H26.61v26.8h80.83v-26.8Z"></path>
|
|
||||||
<path fill="#2e3524" d="M107.44,30.27H26.61v26.8h80.83v-26.8Z"></path>
|
|
||||||
<path
|
|
||||||
fill="#2e3524"
|
|
||||||
d="M134.38,131.23c0-3.72-3.02-6.73-6.73-6.73s-6.73,3.02-6.73,6.73,3.02,6.73,6.73,6.73,6.73-3.02,6.73-6.73Z"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path fill="#2e3524" d="M244.7,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#2e3524"
|
|
||||||
d="M362.72,3.24h57.79c10.71,0,20.47,2.35,29.29,7.06,8.83,4.7,15.79,11.18,20.87,19.39,5.08,8.21,7.63,17.45,7.63,27.67v214.88c0,10.22-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.83,4.7-18.73,7.08-29.7,7.08h-57.79V3.24ZM427.55,283.88c1.74-1.87,2.6-4.18,2.6-6.86V52.56c-.26-2.69-1.34-4.97-3.22-6.86-1.88-1.87-4.15-2.83-6.82-2.83h-14v243.85h14.41c2.93,0,5.27-.94,7.01-2.83h.02Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#2e3524"
|
|
||||||
d="M531.5,322.49c-8.71-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67V3.24h48.15v279.41c0,2.69.93,4.99,2.82,6.86,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.86V3.24h48.16v272.21c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.7-18.73,7.08-29.7,7.08s-20.8-2.35-29.5-7.08l.05-.02Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#2e3524"
|
|
||||||
d="M672.79,322.49c-8.7-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67v-78.75h48.16v85.95c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-77.88c0-5.66-2.22-10.3-6.63-13.94-4.41-3.62-11.57-8.02-21.47-13.13-8.3-4.3-15.05-8.14-20.27-11.52-5.22-3.36-9.71-7.87-13.45-13.54-3.75-5.66-5.63-12.24-5.63-19.8V54.12c0-10.22,2.53-19.44,7.63-27.67,5.08-8.21,11.97-14.66,20.68-19.39,8.68-4.7,18.53-7.06,29.5-7.06s20.87,2.35,29.69,7.06c8.83,4.7,15.72,11.18,20.68,19.39,4.96,8.21,7.42,17.45,7.42,27.67v71.09h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v69.79c0,6.19,2.34,11.26,7.04,15.14,4.67,3.91,12.24,8.83,22.68,14.74,8.04,4.32,14.57,8.09,19.68,11.3,5.08,3.24,9.37,7.46,12.83,12.72,3.48,5.26,5.22,11.26,5.22,17.98v86.83c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.71-18.72,7.08-29.7,7.08s-20.8-2.35-29.5-7.08Z"
|
|
||||||
></path>
|
|
||||||
<path fill="#2e3524" d="M784.56,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#2e3524"
|
|
||||||
d="M920.63,322.49c-5.63-4.18-10.11-10.1-13.45-17.76-3.34-7.68-5.01-16.49-5.01-26.45V53.71c0-9.96,2.53-19.06,7.63-27.26,5.08-8.21,12.05-14.66,20.87-19.39,8.83-4.7,18.6-7.06,29.32-7.06s20.54,2.35,29.5,7.06c8.97,4.7,15.91,11.18,20.87,19.39,4.96,8.21,7.42,17.3,7.42,27.26v94.51h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v231.36c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-46.03h-11.64v-51.29h59.8v145.4h-48.16v-14.14c-2.96,5.4-6.82,9.48-11.64,12.31-4.82,2.83-10.83,4.25-18.06,4.25s-13.64-2.09-19.27-6.26l-.02-.02Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#2e3524"
|
|
||||||
d="M1053.27,25.05h-6.13l-.06-3.69h5.48c.83-.02,1.61-.15,2.33-.4.72-.27,1.3-.64,1.73-1.14.44-.51.65-1.14.65-1.87,0-.93-.16-1.67-.48-2.22-.3-.55-.83-.94-1.59-1.16-.74-.25-1.74-.37-3.01-.37h-3.78v20.42h-4.12V10.54h7.9c1.87,0,3.49.27,4.86.82,1.38.53,2.44,1.34,3.18,2.44.76,1.08,1.14,2.43,1.14,4.06,0,1.02-.24,1.93-.71,2.73-.47.8-1.17,1.49-2.1,2.07-.91.57-2.03,1.03-3.35,1.39-.06,0-.12.07-.2.2-.06.13-.11.2-.17.2-.32.19-.53.33-.63.43-.08.08-.16.12-.25.14-.08.02-.31.03-.68.03ZM1052.99,25.05l.6-2.81c2.95,0,4.97.64,6.05,1.93,1.08,1.27,1.62,2.89,1.62,4.86v1.53c0,.7.03,1.37.08,2.02.08.62.21,1.15.4,1.59v.45h-4.23c-.19-.49-.3-1.19-.34-2.1-.02-.91-.03-1.57-.03-1.99v-1.48c0-1.38-.31-2.39-.94-3.04s-1.69-.97-3.21-.97ZM1035.75,22.92c0,2.52.43,4.87,1.28,7.04.87,2.16,2.08,4.05,3.64,5.68,1.55,1.61,3.34,2.87,5.37,3.78,2.05.89,4.22,1.33,6.53,1.33s4.51-.44,6.53-1.33c2.03-.91,3.8-2.17,5.34-3.78,1.53-1.63,2.74-3.52,3.61-5.68.87-2.18,1.31-4.52,1.31-7.04s-.44-4.86-1.31-7.01c-.87-2.16-2.07-4.04-3.61-5.65-1.53-1.61-3.31-2.86-5.34-3.75-2.03-.91-4.2-1.36-6.53-1.36s-4.49.45-6.53,1.36c-2.03.89-3.81,2.14-5.37,3.75-1.55,1.61-2.77,3.49-3.64,5.65-.85,2.16-1.28,4.5-1.28,7.01ZM1032.4,22.92c0-3.01.52-5.8,1.56-8.38,1.04-2.57,2.49-4.82,4.34-6.73,1.86-1.93,4-3.43,6.42-4.49,2.44-1.08,5.06-1.62,7.84-1.62s5.39.54,7.81,1.62c2.44,1.06,4.58,2.56,6.42,4.49,1.86,1.91,3.31,4.16,4.35,6.73,1.06,2.57,1.59,5.37,1.59,8.38s-.53,5.8-1.59,8.38c-1.04,2.57-2.49,4.84-4.35,6.79-1.83,1.93-3.97,3.44-6.42,4.52-2.42,1.08-5.03,1.62-7.81,1.62s-5.39-.54-7.84-1.62c-2.42-1.08-4.56-2.58-6.42-4.52-1.85-1.95-3.3-4.21-4.34-6.79-1.04-2.57-1.56-5.37-1.56-8.38Z"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,19 +0,0 @@
|
|||||||
import { Skeleton as XSkeleton } from '@repo/ui/components/ui/skeleton'
|
|
||||||
|
|
||||||
export function Skeleton() {
|
|
||||||
return (
|
|
||||||
<div className="lg:max-w-2xl mx-auto flex flex-col space-y-3">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<XSkeleton className="h-4 w-2/6" />
|
|
||||||
<XSkeleton className="h-4 w-4/6" />
|
|
||||||
<XSkeleton className="h-4 w-5/6" />
|
|
||||||
<XSkeleton className="h-4 w-6/6" />
|
|
||||||
<XSkeleton className="h-4 w-6/6" />
|
|
||||||
<XSkeleton className="h-4 w-3/6" />
|
|
||||||
<XSkeleton className="h-4 w-5/6" />
|
|
||||||
<XSkeleton className="h-4 w-4/6" />
|
|
||||||
<XSkeleton className="h-4 w-4/6" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
const MOBILE_BREAKPOINT = 768
|
|
||||||
|
|
||||||
export function useIsMobile() {
|
|
||||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
||||||
const onChange = () => {
|
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
||||||
}
|
|
||||||
mql.addEventListener('change', onChange)
|
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
||||||
return () => mql.removeEventListener('change', onChange)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return !!isMobile
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { requestIdContext, userContext } from '@/context'
|
import type { User } from '@repo/auth/auth'
|
||||||
import type { User } from '@/lib/auth'
|
import { requestIdContext, userContext } from '@repo/auth/context'
|
||||||
|
|
||||||
import type { LoaderFunctionArgs } from 'react-router'
|
import type { LoaderFunctionArgs } from 'react-router'
|
||||||
|
|
||||||
export enum HttpMethod {
|
export enum HttpMethod {
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
ScrollRestoration
|
ScrollRestoration
|
||||||
} from 'react-router'
|
} from 'react-router'
|
||||||
|
|
||||||
|
import { loggingMiddleware } from '@repo/auth/middleware/logging'
|
||||||
import { ThemeProvider } from '@repo/ui/components/theme-provider'
|
import { ThemeProvider } from '@repo/ui/components/theme-provider'
|
||||||
import './app.css'
|
import './app.css'
|
||||||
import { loggingMiddleware } from './middleware/logging'
|
|
||||||
|
|
||||||
export const middleware: Route.MiddlewareFunction[] = [loggingMiddleware]
|
export const middleware: Route.MiddlewareFunction[] = [loggingMiddleware]
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>saladeaula.digital</title>
|
<title>admin.saladeaula.digital</title>
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { Suspense, useMemo } from 'react'
|
|||||||
import { Await, useSearchParams } from 'react-router'
|
import { Await, useSearchParams } from 'react-router'
|
||||||
|
|
||||||
import placeholder from '@/assets/placeholder.webp'
|
import placeholder from '@/assets/placeholder.webp'
|
||||||
import { SearchForm } from '@/components/search-form'
|
|
||||||
import { Skeleton } from '@/components/skeleton'
|
|
||||||
import { createSearch } from '@/lib/meili'
|
import { createSearch } from '@/lib/meili'
|
||||||
import { request as req } from '@/lib/request'
|
import { request as req } from '@/lib/request'
|
||||||
|
|
||||||
|
import { SearchForm } from '@repo/ui/components/search-form'
|
||||||
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import { Suspense, useState } from 'react'
|
|||||||
import { Await, Link, useSearchParams } from 'react-router'
|
import { Await, Link, useSearchParams } from 'react-router'
|
||||||
|
|
||||||
import { CustomizeColumns, DataTable } from '@/components/data-table'
|
import { CustomizeColumns, DataTable } from '@/components/data-table'
|
||||||
import { FacetedFilter } from '@/components/faceted-filter'
|
|
||||||
import { RangeCalendarFilter } from '@/components/range-calendar-filter'
|
import { RangeCalendarFilter } from '@/components/range-calendar-filter'
|
||||||
import { SearchForm } from '@/components/search-form'
|
|
||||||
import { Skeleton } from '@/components/skeleton'
|
|
||||||
import { createSearch } from '@/lib/meili'
|
import { createSearch } from '@/lib/meili'
|
||||||
|
|
||||||
|
import { FacetedFilter } from '@repo/ui/components/faceted-filter'
|
||||||
|
import { SearchForm } from '@repo/ui/components/search-form'
|
||||||
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||||
import {
|
import {
|
||||||
@@ -128,33 +129,6 @@ export default function Route({ loaderData: { data } }) {
|
|||||||
|
|
||||||
<div className="flex gap-2.5 max-lg:flex-col w-full">
|
<div className="flex gap-2.5 max-lg:flex-col w-full">
|
||||||
<div className="flex gap-2.5 max-lg:flex-col">
|
<div className="flex gap-2.5 max-lg:flex-col">
|
||||||
<FacetedFilter
|
|
||||||
icon={BookCopyIcon}
|
|
||||||
className="lg:flex-1"
|
|
||||||
value={searchParams.getAll('courses')}
|
|
||||||
onChange={(courses) => {
|
|
||||||
setSearchParams((searchParams) => {
|
|
||||||
searchParams.delete('courses')
|
|
||||||
searchParams.delete('p')
|
|
||||||
|
|
||||||
if (statuses.length) {
|
|
||||||
courses.forEach((s) =>
|
|
||||||
searchParams.has('courses', s)
|
|
||||||
? null
|
|
||||||
: searchParams.append('courses', s)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return searchParams
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
title="Cursos"
|
|
||||||
options={Object.entries(statuses).map(([key, value]) => ({
|
|
||||||
value: key,
|
|
||||||
...value
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FacetedFilter
|
<FacetedFilter
|
||||||
icon={PlusCircleIcon}
|
icon={PlusCircleIcon}
|
||||||
className="lg:flex-1"
|
className="lg:flex-1"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { Suspense } from 'react'
|
|||||||
import { Await } from 'react-router'
|
import { Await } from 'react-router'
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table'
|
import { DataTable } from '@/components/data-table'
|
||||||
import { Skeleton } from '@/components/skeleton'
|
|
||||||
import { createSearch } from '@/lib/meili'
|
import { createSearch } from '@/lib/meili'
|
||||||
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
import { columns, type Order } from './columns'
|
import { columns, type Order } from './columns'
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import type { Route } from './+types'
|
|||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
import { Await } from 'react-router'
|
import { Await } from 'react-router'
|
||||||
|
|
||||||
import { Skeleton } from '@/components/skeleton'
|
|
||||||
import { request as req } from '@/lib/request'
|
import { request as req } from '@/lib/request'
|
||||||
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
|
|
||||||
export async function loader({ params, request, context }: Route.LoaderArgs) {
|
export async function loader({ params, request, context }: Route.LoaderArgs) {
|
||||||
const { id } = params
|
const { id } = params
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { Await } from 'react-router'
|
|||||||
|
|
||||||
import type { Route } from './+types'
|
import type { Route } from './+types'
|
||||||
|
|
||||||
import { Skeleton } from '@/components/skeleton'
|
|
||||||
import { request as req } from '@/lib/request'
|
import { request as req } from '@/lib/request'
|
||||||
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
export async function loader({ params, request, context }: Route.LoaderArgs) {
|
export async function loader({ params, request, context }: Route.LoaderArgs) {
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import { Suspense } from 'react'
|
|||||||
import { Await, Link, useSearchParams } from 'react-router'
|
import { Await, Link, useSearchParams } from 'react-router'
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table'
|
import { DataTable } from '@/components/data-table'
|
||||||
import { SearchForm } from '@/components/search-form'
|
|
||||||
import { Skeleton } from '@/components/skeleton'
|
|
||||||
import { createSearch } from '@/lib/meili'
|
import { createSearch } from '@/lib/meili'
|
||||||
|
import { columns, type User } from './columns'
|
||||||
|
|
||||||
|
import { SearchForm } from '@repo/ui/components/search-form'
|
||||||
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||||
import { columns, type User } from './columns'
|
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ import type { Route } from './+types'
|
|||||||
import { useLoaderData } from 'react-router'
|
import { useLoaderData } from 'react-router'
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table'
|
import { DataTable } from '@/components/data-table'
|
||||||
import { authMiddleware } from '@/middleware/auth'
|
|
||||||
import { columns, type Webhook } from './columns'
|
import { columns, type Webhook } from './columns'
|
||||||
|
|
||||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [{ title: 'Webhooks' }]
|
return [{ title: 'Webhooks' }]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ import type { Route } from './+types'
|
|||||||
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
|
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
|
||||||
|
|
||||||
import { AppSidebar } from '@/components/app-sidebar'
|
import { AppSidebar } from '@/components/app-sidebar'
|
||||||
import { ModeToggle, ThemedImage } from '@/components/dark-mode'
|
|
||||||
import { NavUser } from '@/components/nav-user'
|
|
||||||
import { userContext } from '@/context'
|
|
||||||
import { useIsMobile } from '@/hooks/use-mobile'
|
|
||||||
import { request as req } from '@/lib/request'
|
import { request as req } from '@/lib/request'
|
||||||
import { authMiddleware } from '@/middleware/auth'
|
|
||||||
|
import { userContext } from '@repo/auth/context'
|
||||||
|
import { authMiddleware } from '@repo/auth/middleware/auth'
|
||||||
|
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
|
||||||
|
import { NavUser } from '@repo/ui/components/nav-user'
|
||||||
import {
|
import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarTrigger
|
SidebarTrigger
|
||||||
} from '@repo/ui/components/ui/sidebar'
|
} from '@repo/ui/components/ui/sidebar'
|
||||||
|
import { useIsMobile } from '@repo/ui/hooks/use-mobile'
|
||||||
|
|
||||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type { Route } from './+types'
|
|||||||
|
|
||||||
import { redirect } from 'react-router'
|
import { redirect } from 'react-router'
|
||||||
|
|
||||||
import { userContext } from '@/context'
|
|
||||||
import { request as req } from '@/lib/request'
|
import { request as req } from '@/lib/request'
|
||||||
import { authMiddleware } from '@/middleware/auth'
|
import { userContext } from '@repo/auth/context'
|
||||||
|
import { authMiddleware } from '@repo/auth/middleware/auth'
|
||||||
|
|
||||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type { Route } from './+types'
|
|||||||
|
|
||||||
import { redirect } from 'react-router'
|
import { redirect } from 'react-router'
|
||||||
|
|
||||||
import { requestIdContext } from '@/context'
|
import { createAuth, type User } from '@repo/auth/auth'
|
||||||
import { createAuth, type User } from '@/lib/auth'
|
import { requestIdContext } from '@repo/auth/context'
|
||||||
import { createSessionStorage } from '@/lib/session'
|
import { createSessionStorage } from '@repo/auth/session'
|
||||||
|
|
||||||
export async function loader({ request, context }: Route.ActionArgs) {
|
export async function loader({ request, context }: Route.ActionArgs) {
|
||||||
const sessionStorage = createSessionStorage(context.cloudflare.env)
|
const sessionStorage = createSessionStorage(context.cloudflare.env)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import type { Route } from './+types'
|
|||||||
import { redirect } from 'react-router'
|
import { redirect } from 'react-router'
|
||||||
import type { OAuth2Strategy } from 'remix-auth-oauth2'
|
import type { OAuth2Strategy } from 'remix-auth-oauth2'
|
||||||
|
|
||||||
import { createAuth, type User } from '@/lib/auth'
|
import { createAuth, type User } from '@repo/auth/auth'
|
||||||
import { createSessionStorage } from '@/lib/session'
|
import { createSessionStorage } from '@repo/auth/session'
|
||||||
|
|
||||||
export async function loader({ request, context }: Route.LoaderArgs) {
|
export async function loader({ request, context }: Route.LoaderArgs) {
|
||||||
const authenticator = createAuth(context.cloudflare.env)
|
const authenticator = createAuth(context.cloudflare.env)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Route } from './+types'
|
import type { Route } from './+types'
|
||||||
|
|
||||||
import { userContext } from '@/context'
|
import type { User } from '@repo/auth/auth'
|
||||||
import type { User } from '@/lib/auth'
|
import { userContext } from '@repo/auth/context'
|
||||||
import { authMiddleware } from '@/middleware/auth'
|
import { authMiddleware } from '@repo/auth/middleware/auth'
|
||||||
|
|
||||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||||
export const loader = proxy
|
export const loader = proxy
|
||||||
|
|||||||
@@ -12,26 +12,20 @@
|
|||||||
"typecheck": "npm run cf-typegen && react-router typegen && tsc -b"
|
"typecheck": "npm run cf-typegen && react-router typegen && tsc -b"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@brazilian-utils/brazilian-utils": "^1.0.0-rc.12",
|
|
||||||
"@hookform/resolvers": "^5.2.2",
|
|
||||||
"@react-router/fs-routes": "^7.9.5",
|
"@react-router/fs-routes": "^7.9.5",
|
||||||
"@repo/ui": "*",
|
"@repo/ui": "*",
|
||||||
|
"@repo/auth": "*",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"http-status-codes": "^2.3.0",
|
"http-status-codes": "^2.3.0",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
"jose": "^6.1.0",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"meilisearch": "^0.54.0",
|
"meilisearch": "^0.54.0",
|
||||||
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.66.0",
|
|
||||||
"react-number-format": "^5.4.4",
|
|
||||||
"react-router": "^7.9.5",
|
"react-router": "^7.9.5",
|
||||||
"remix-auth-oauth2": "^3.4.1",
|
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# [id.saladeaula.digital](https://insights.saladeaula.digital)
|
# [insights.saladeaula.digital](https://insights.saladeaula.digital)
|
||||||
|
|
||||||
O código-fonte para [id.saladeaula.digital](https://insights.saladeaula.digital), construído com [React Router](https://github.com/remix-run/react-router).
|
O código-fonte para [insights.saladeaula.digital](https://insights.saladeaula.digital), construído com [React Router](https://github.com/remix-run/react-router).
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@repo/ui": "*",
|
"@repo/ui": "*",
|
||||||
|
"@repo/auth": "*",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from '@repo/ui/postcss.config'
|
|
||||||
@@ -1,133 +1,11 @@
|
|||||||
@import 'tailwindcss' source('.');
|
@import 'tailwindcss';
|
||||||
@import 'tw-animate-css';
|
@import '@repo/ui/globals.css';
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
/**
|
||||||
|
* This is necessary to load the @repo/ui package when moving from
|
||||||
@theme {
|
* @tailwindcss/vite v4.0.7 to v4.0.8.
|
||||||
--font-sans:
|
*
|
||||||
'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
* For more details, see:
|
||||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
* https://github.com/tailwindlabs/tailwindcss/issues/16733
|
||||||
}
|
*/
|
||||||
|
@source '../../../packages/ui';
|
||||||
html,
|
|
||||||
body {
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@theme inline {
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--color-card: var(--card);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
--color-chart-1: var(--chart-1);
|
|
||||||
--color-chart-2: var(--chart-2);
|
|
||||||
--color-chart-3: var(--chart-3);
|
|
||||||
--color-chart-4: var(--chart-4);
|
|
||||||
--color-chart-5: var(--chart-5);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--radius: 0.625rem;
|
|
||||||
--background: oklch(1 0 0);
|
|
||||||
--foreground: oklch(0.145 0 0);
|
|
||||||
--card: oklch(1 0 0);
|
|
||||||
--card-foreground: oklch(0.145 0 0);
|
|
||||||
--popover: oklch(1 0 0);
|
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
|
||||||
--primary: oklch(0.205 0 0);
|
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
|
||||||
--secondary: oklch(0.97 0 0);
|
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
|
||||||
--muted: oklch(0.97 0 0);
|
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
|
||||||
--accent: oklch(0.97 0 0);
|
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
|
||||||
--border: oklch(0.922 0 0);
|
|
||||||
--input: oklch(0.922 0 0);
|
|
||||||
--ring: oklch(0.708 0 0);
|
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
|
||||||
--sidebar: oklch(0.985 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: oklch(0.145 0 0);
|
|
||||||
--foreground: oklch(0.985 0 0);
|
|
||||||
--card: oklch(0.205 0 0);
|
|
||||||
--card-foreground: oklch(0.985 0 0);
|
|
||||||
--popover: oklch(0.205 0 0);
|
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
|
||||||
--primary: oklch(0.922 0 0);
|
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
|
||||||
--secondary: oklch(0.269 0 0);
|
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
|
||||||
--muted: oklch(0.269 0 0);
|
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
|
||||||
--accent: oklch(0.269 0 0);
|
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
|
||||||
--border: oklch(1 0 0 / 10%);
|
|
||||||
--input: oklch(1 0 0 / 15%);
|
|
||||||
--ring: oklch(0.556 0 0);
|
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
|
||||||
--sidebar: oklch(0.205 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
@apply border-border outline-ring/50;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
@apply bg-background text-foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Check, PlusCircle } from 'lucide-react'
|
import { Check, PlusCircle } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@repo/ui/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -11,13 +11,13 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator
|
CommandSeparator
|
||||||
} from '@/components/ui/command'
|
} from '@repo/ui/components/ui/command'
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger
|
PopoverTrigger
|
||||||
} from '@/components/ui/popover'
|
} from '@repo/ui/components/ui/popover'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@repo/ui/components/ui/separator'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface FacetedFilterProps<TData, TValue> {
|
interface FacetedFilterProps<TData, TValue> {
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1072.73 329.6" width="111" height="36" title="EDUSEG">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path
|
|
||||||
fill="#8cd366"
|
|
||||||
d="M152.18,217.62l-68.61-30.91c-1.88-1.2-4.28-1.2-6.16,0l-68.61,30.91c-3.81,2.43-8.8-.3-8.8-4.82V8.98C0,5.82,2.56,3.26,5.72,3.26h149.54c3.16,0,5.72,2.56,5.72,5.72v203.81c0,4.52-5,7.26-8.8,4.82Z"
|
|
||||||
></path>
|
|
||||||
<path fill="#2e3524" d="M93.97,74.01H26.61v20.16h67.36v-20.16Z"></path>
|
|
||||||
<path fill="#2e3524" d="M107.44,111.16H26.61v26.8h80.83v-26.8Z"></path>
|
|
||||||
<path fill="#2e3524" d="M107.44,30.27H26.61v26.8h80.83v-26.8Z"></path>
|
|
||||||
<path
|
|
||||||
fill="#2e3524"
|
|
||||||
d="M134.38,131.23c0-3.72-3.02-6.73-6.73-6.73s-6.73,3.02-6.73,6.73,3.02,6.73,6.73,6.73,6.73-3.02,6.73-6.73Z"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path fill="#f9f7e8" d="M244.7,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#f9f7e8"
|
|
||||||
d="M362.72,3.24h57.79c10.71,0,20.47,2.35,29.29,7.06,8.83,4.7,15.79,11.18,20.87,19.39,5.08,8.21,7.63,17.45,7.63,27.67v214.88c0,10.22-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.83,4.7-18.73,7.08-29.7,7.08h-57.79V3.24ZM427.55,283.88c1.74-1.87,2.6-4.18,2.6-6.86V52.56c-.26-2.69-1.34-4.97-3.22-6.86-1.88-1.87-4.15-2.83-6.82-2.83h-14v243.85h14.41c2.93,0,5.27-.94,7.01-2.83h.02Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#f9f7e8"
|
|
||||||
d="M531.5,322.49c-8.71-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67V3.24h48.15v279.41c0,2.69.93,4.99,2.82,6.86,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.86V3.24h48.16v272.21c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.7-18.73,7.08-29.7,7.08s-20.8-2.35-29.5-7.08l.05-.02Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#f9f7e8"
|
|
||||||
d="M672.79,322.49c-8.7-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67v-78.75h48.16v85.95c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-77.88c0-5.66-2.22-10.3-6.63-13.94-4.41-3.62-11.57-8.02-21.47-13.13-8.3-4.3-15.05-8.14-20.27-11.52-5.22-3.36-9.71-7.87-13.45-13.54-3.75-5.66-5.63-12.24-5.63-19.8V54.12c0-10.22,2.53-19.44,7.63-27.67,5.08-8.21,11.97-14.66,20.68-19.39,8.68-4.7,18.53-7.06,29.5-7.06s20.87,2.35,29.69,7.06c8.83,4.7,15.72,11.18,20.68,19.39,4.96,8.21,7.42,17.45,7.42,27.67v71.09h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v69.79c0,6.19,2.34,11.26,7.04,15.14,4.67,3.91,12.24,8.83,22.68,14.74,8.04,4.32,14.57,8.09,19.68,11.3,5.08,3.24,9.37,7.46,12.83,12.72,3.48,5.26,5.22,11.26,5.22,17.98v86.83c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.71-18.72,7.08-29.7,7.08s-20.8-2.35-29.5-7.08Z"
|
|
||||||
></path>
|
|
||||||
<path fill="#f9f7e8" d="M784.56,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#f9f7e8"
|
|
||||||
d="M920.63,322.49c-5.63-4.18-10.11-10.1-13.45-17.76-3.34-7.68-5.01-16.49-5.01-26.45V53.71c0-9.96,2.53-19.06,7.63-27.26,5.08-8.21,12.05-14.66,20.87-19.39,8.83-4.7,18.6-7.06,29.32-7.06s20.54,2.35,29.5,7.06c8.97,4.7,15.91,11.18,20.87,19.39,4.96,8.21,7.42,17.3,7.42,27.26v94.51h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v231.36c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-46.03h-11.64v-51.29h59.8v145.4h-48.16v-14.14c-2.96,5.4-6.82,9.48-11.64,12.31-4.82,2.83-10.83,4.25-18.06,4.25s-13.64-2.09-19.27-6.26l-.02-.02Z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="#f9f7e8"
|
|
||||||
d="M1053.27,25.05h-6.13l-.06-3.69h5.48c.83-.02,1.61-.15,2.33-.4.72-.27,1.3-.64,1.73-1.14.44-.51.65-1.14.65-1.87,0-.93-.16-1.67-.48-2.22-.3-.55-.83-.94-1.59-1.16-.74-.25-1.74-.37-3.01-.37h-3.78v20.42h-4.12V10.54h7.9c1.87,0,3.49.27,4.86.82,1.38.53,2.44,1.34,3.18,2.44.76,1.08,1.14,2.43,1.14,4.06,0,1.02-.24,1.93-.71,2.73-.47.8-1.17,1.49-2.1,2.07-.91.57-2.03,1.03-3.35,1.39-.06,0-.12.07-.2.2-.06.13-.11.2-.17.2-.32.19-.53.33-.63.43-.08.08-.16.12-.25.14-.08.02-.31.03-.68.03ZM1052.99,25.05l.6-2.81c2.95,0,4.97.64,6.05,1.93,1.08,1.27,1.62,2.89,1.62,4.86v1.53c0,.7.03,1.37.08,2.02.08.62.21,1.15.4,1.59v.45h-4.23c-.19-.49-.3-1.19-.34-2.1-.02-.91-.03-1.57-.03-1.99v-1.48c0-1.38-.31-2.39-.94-3.04s-1.69-.97-3.21-.97ZM1035.75,22.92c0,2.52.43,4.87,1.28,7.04.87,2.16,2.08,4.05,3.64,5.68,1.55,1.61,3.34,2.87,5.37,3.78,2.05.89,4.22,1.33,6.53,1.33s4.51-.44,6.53-1.33c2.03-.91,3.8-2.17,5.34-3.78,1.53-1.63,2.74-3.52,3.61-5.68.87-2.18,1.31-4.52,1.31-7.04s-.44-4.86-1.31-7.01c-.87-2.16-2.07-4.04-3.61-5.65-1.53-1.61-3.31-2.86-5.34-3.75-2.03-.91-4.2-1.36-6.53-1.36s-4.49.45-6.53,1.36c-2.03.89-3.81,2.14-5.37,3.75-1.55,1.61-2.77,3.49-3.64,5.65-.85,2.16-1.28,4.5-1.28,7.01ZM1032.4,22.92c0-3.01.52-5.8,1.56-8.38,1.04-2.57,2.49-4.82,4.34-6.73,1.86-1.93,4-3.43,6.42-4.49,2.44-1.08,5.06-1.62,7.84-1.62s5.39.54,7.81,1.62c2.44,1.06,4.58,2.56,6.42,4.49,1.86,1.91,3.31,4.16,4.35,6.73,1.06,2.57,1.59,5.37,1.59,8.38s-.53,5.8-1.59,8.38c-1.04,2.57-2.49,4.84-4.35,6.79-1.83,1.93-3.97,3.44-6.42,4.52-2.42,1.08-5.03,1.62-7.81,1.62s-5.39-.54-7.84-1.62c-2.42-1.08-4.56-2.58-6.42-4.52-1.85-1.95-3.3-4.21-4.34-6.79-1.04-2.57-1.56-5.37-1.56-8.38Z"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,141 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import {
|
|
||||||
CirclePlayIcon,
|
|
||||||
DollarSignIcon,
|
|
||||||
LayoutDashboardIcon,
|
|
||||||
LogOutIcon,
|
|
||||||
UserIcon
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { Link } from 'react-router'
|
|
||||||
|
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger
|
|
||||||
} from '@/components/ui/dropdown-menu'
|
|
||||||
import { initials } from '@/lib/utils'
|
|
||||||
|
|
||||||
export function NavUser({
|
|
||||||
user
|
|
||||||
}: {
|
|
||||||
user: {
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
scope: string
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
const scopes = user.scope.split(' ')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger className="cursor-pointer" asChild>
|
|
||||||
<Avatar className="size-10">
|
|
||||||
<AvatarFallback>{initials(user.name)}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
|
||||||
side="bottom"
|
|
||||||
align="end"
|
|
||||||
sideOffset={4}
|
|
||||||
>
|
|
||||||
<DropdownMenuLabel className="p-0 font-normal">
|
|
||||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
||||||
<Avatar className="size-10">
|
|
||||||
<AvatarFallback>{initials(user.name)}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
||||||
<span className="truncate font-medium">{user.name}</span>
|
|
||||||
<span className="truncate text-xs text-muted-foreground">
|
|
||||||
{user.email}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Link to="/settings" className="cursor-pointer">
|
|
||||||
<UserIcon />
|
|
||||||
Minha conta
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Link to="/payments" className="cursor-pointer">
|
|
||||||
<DollarSignIcon />
|
|
||||||
Histórico de compras
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
{grantIfHas(['apps:admin', 'apps:studio'], scopes, 'any') && (
|
|
||||||
<>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuLabel className="text-white/50 text-sm">
|
|
||||||
Aplicações
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{grantIfHas(['apps:admin'], scopes) && (
|
|
||||||
<>
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Link
|
|
||||||
to="//admin.saladeaula.digital"
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<LayoutDashboardIcon />
|
|
||||||
Administrador
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{grantIfHas(['apps:studio'], scopes) && (
|
|
||||||
<>
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Link
|
|
||||||
to="//studio.saladeaula.digital"
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<CirclePlayIcon />
|
|
||||||
EDUSEG® Estúdio
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Link to="/logout" className="cursor-pointer" reloadDocument>
|
|
||||||
<LogOutIcon />
|
|
||||||
Sair
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function grantIfHas(
|
|
||||||
required: string[],
|
|
||||||
granted: string[],
|
|
||||||
mode: 'all' | 'any' = 'all'
|
|
||||||
): boolean {
|
|
||||||
const grantedSet: Set<string> = new Set(granted)
|
|
||||||
|
|
||||||
if (mode === 'all') {
|
|
||||||
return required.every((scope) => grantedSet.has(scope))
|
|
||||||
}
|
|
||||||
|
|
||||||
return required.some((scope) => grantedSet.has(scope))
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
InputGroupInput
|
InputGroupInput
|
||||||
} from '@/components/ui/input-group'
|
} from '@repo/ui/components/ui/input-group'
|
||||||
import { useKeyPress } from '@/hooks/use-keypress'
|
import { useKeyPress } from '@/hooks/use-keypress'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Avatar({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Root
|
|
||||||
data-slot="avatar"
|
|
||||||
className={cn(
|
|
||||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarImage({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Image
|
|
||||||
data-slot="avatar-image"
|
|
||||||
className={cn("aspect-square size-full", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarFallback({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Fallback
|
|
||||||
data-slot="avatar-fallback"
|
|
||||||
className={cn(
|
|
||||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const badgeVariants = cva(
|
|
||||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
|
||||||
secondary:
|
|
||||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
||||||
destructive:
|
|
||||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
||||||
outline:
|
|
||||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Badge({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span"> &
|
|
||||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
||||||
const Comp = asChild ? Slot : "span"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="badge"
|
|
||||||
className={cn(badgeVariants({ variant }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
|
||||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
|
||||||
return (
|
|
||||||
<ol
|
|
||||||
data-slot="breadcrumb-list"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
data-slot="breadcrumb-item"
|
|
||||||
className={cn("inline-flex items-center gap-1.5", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbLink({
|
|
||||||
asChild,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"a"> & {
|
|
||||||
asChild?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "a"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="breadcrumb-link"
|
|
||||||
className={cn("hover:text-foreground transition-colors", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="breadcrumb-page"
|
|
||||||
role="link"
|
|
||||||
aria-disabled="true"
|
|
||||||
aria-current="page"
|
|
||||||
className={cn("text-foreground font-normal", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbSeparator({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"li">) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
data-slot="breadcrumb-separator"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
className={cn("[&>svg]:size-3.5", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? <ChevronRight />}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbEllipsis({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="breadcrumb-ellipsis"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
className={cn("flex size-9 items-center justify-center", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="size-4" />
|
|
||||||
<span className="sr-only">More</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
BreadcrumbEllipsis,
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
||||||
destructive:
|
|
||||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
||||||
outline:
|
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
||||||
secondary:
|
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
||||||
ghost:
|
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
||||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
||||||
icon: "size-9",
|
|
||||||
"icon-sm": "size-8",
|
|
||||||
"icon-lg": "size-10",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Button({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"button"> &
|
|
||||||
VariantProps<typeof buttonVariants> & {
|
|
||||||
asChild?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="button"
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card"
|
|
||||||
className={cn(
|
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-header"
|
|
||||||
className={cn(
|
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-title"
|
|
||||||
className={cn("leading-none font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-action"
|
|
||||||
className={cn(
|
|
||||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-content"
|
|
||||||
className={cn("px-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-footer"
|
|
||||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
CardTitle,
|
|
||||||
CardAction,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Command as CommandPrimitive } from "cmdk"
|
|
||||||
import { SearchIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog"
|
|
||||||
|
|
||||||
function Command({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
|
||||||
return (
|
|
||||||
<CommandPrimitive
|
|
||||||
data-slot="command"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CommandDialog({
|
|
||||||
title = "Command Palette",
|
|
||||||
description = "Search for a command to run...",
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
showCloseButton = true,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Dialog> & {
|
|
||||||
title?: string
|
|
||||||
description?: string
|
|
||||||
className?: string
|
|
||||||
showCloseButton?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Dialog {...props}>
|
|
||||||
<DialogHeader className="sr-only">
|
|
||||||
<DialogTitle>{title}</DialogTitle>
|
|
||||||
<DialogDescription>{description}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogContent
|
|
||||||
className={cn("overflow-hidden p-0", className)}
|
|
||||||
showCloseButton={showCloseButton}
|
|
||||||
>
|
|
||||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
||||||
{children}
|
|
||||||
</Command>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CommandInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="command-input-wrapper"
|
|
||||||
className="flex h-9 items-center gap-2 border-b px-3"
|
|
||||||
>
|
|
||||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
|
||||||
<CommandPrimitive.Input
|
|
||||||
data-slot="command-input"
|
|
||||||
className={cn(
|
|
||||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CommandList({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
|
||||||
return (
|
|
||||||
<CommandPrimitive.List
|
|
||||||
data-slot="command-list"
|
|
||||||
className={cn(
|
|
||||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CommandEmpty({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
|
||||||
return (
|
|
||||||
<CommandPrimitive.Empty
|
|
||||||
data-slot="command-empty"
|
|
||||||
className="py-6 text-center text-sm"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CommandGroup({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
|
||||||
return (
|
|
||||||
<CommandPrimitive.Group
|
|
||||||
data-slot="command-group"
|
|
||||||
className={cn(
|
|
||||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CommandSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<CommandPrimitive.Separator
|
|
||||||
data-slot="command-separator"
|
|
||||||
className={cn("bg-border -mx-1 h-px", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CommandItem({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
|
||||||
return (
|
|
||||||
<CommandPrimitive.Item
|
|
||||||
data-slot="command-item"
|
|
||||||
className={cn(
|
|
||||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CommandShortcut({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="command-shortcut"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Command,
|
|
||||||
CommandDialog,
|
|
||||||
CommandInput,
|
|
||||||
CommandList,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandItem,
|
|
||||||
CommandShortcut,
|
|
||||||
CommandSeparator,
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
||||||
import { XIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Dialog({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogClose({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogOverlay({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Overlay
|
|
||||||
data-slot="dialog-overlay"
|
|
||||||
className={cn(
|
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
showCloseButton = true,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
||||||
showCloseButton?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DialogPortal data-slot="dialog-portal">
|
|
||||||
<DialogOverlay />
|
|
||||||
<DialogPrimitive.Content
|
|
||||||
data-slot="dialog-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{showCloseButton && (
|
|
||||||
<DialogPrimitive.Close
|
|
||||||
data-slot="dialog-close"
|
|
||||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
)}
|
|
||||||
</DialogPrimitive.Content>
|
|
||||||
</DialogPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="dialog-header"
|
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="dialog-footer"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogTitle({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
data-slot="dialog-title"
|
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Description
|
|
||||||
data-slot="dialog-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogPortal,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
}
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function DropdownMenu({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
|
||||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Trigger
|
|
||||||
data-slot="dropdown-menu-trigger"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuContent({
|
|
||||||
className,
|
|
||||||
sideOffset = 4,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
data-slot="dropdown-menu-content"
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuItem({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
variant = "default",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
variant?: "default" | "destructive"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
data-slot="dropdown-menu-item"
|
|
||||||
data-inset={inset}
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuCheckboxItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
checked,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.RadioGroup
|
|
||||||
data-slot="dropdown-menu-radio-group"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
data-slot="dropdown-menu-radio-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<CircleIcon className="size-2 fill-current" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuLabel({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
data-slot="dropdown-menu-label"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
data-slot="dropdown-menu-separator"
|
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuShortcut({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="dropdown-menu-shortcut"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSub({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
|
||||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubTrigger({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
data-slot="dropdown-menu-sub-trigger"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRightIcon className="ml-auto size-4" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
data-slot="dropdown-menu-sub-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="empty"
|
|
||||||
className={cn(
|
|
||||||
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="empty-header"
|
|
||||||
className={cn(
|
|
||||||
"flex max-w-sm flex-col items-center gap-2 text-center",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyMediaVariants = cva(
|
|
||||||
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-transparent",
|
|
||||||
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function EmptyMedia({
|
|
||||||
className,
|
|
||||||
variant = "default",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="empty-icon"
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(emptyMediaVariants({ variant, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="empty-title"
|
|
||||||
className={cn("text-lg font-medium tracking-tight", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="empty-description"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="empty-content"
|
|
||||||
className={cn(
|
|
||||||
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Empty,
|
|
||||||
EmptyHeader,
|
|
||||||
EmptyTitle,
|
|
||||||
EmptyDescription,
|
|
||||||
EmptyContent,
|
|
||||||
EmptyMedia,
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import {
|
|
||||||
Controller,
|
|
||||||
FormProvider,
|
|
||||||
useFormContext,
|
|
||||||
useFormState,
|
|
||||||
type ControllerProps,
|
|
||||||
type FieldPath,
|
|
||||||
type FieldValues,
|
|
||||||
} from "react-hook-form"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
|
|
||||||
const Form = FormProvider
|
|
||||||
|
|
||||||
type FormFieldContextValue<
|
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
||||||
> = {
|
|
||||||
name: TName
|
|
||||||
}
|
|
||||||
|
|
||||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
|
||||||
{} as FormFieldContextValue
|
|
||||||
)
|
|
||||||
|
|
||||||
const FormField = <
|
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
||||||
>({
|
|
||||||
...props
|
|
||||||
}: ControllerProps<TFieldValues, TName>) => {
|
|
||||||
return (
|
|
||||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
|
||||||
<Controller {...props} />
|
|
||||||
</FormFieldContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const useFormField = () => {
|
|
||||||
const fieldContext = React.useContext(FormFieldContext)
|
|
||||||
const itemContext = React.useContext(FormItemContext)
|
|
||||||
const { getFieldState } = useFormContext()
|
|
||||||
const formState = useFormState({ name: fieldContext.name })
|
|
||||||
const fieldState = getFieldState(fieldContext.name, formState)
|
|
||||||
|
|
||||||
if (!fieldContext) {
|
|
||||||
throw new Error("useFormField should be used within <FormField>")
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = itemContext
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name: fieldContext.name,
|
|
||||||
formItemId: `${id}-form-item`,
|
|
||||||
formDescriptionId: `${id}-form-item-description`,
|
|
||||||
formMessageId: `${id}-form-item-message`,
|
|
||||||
...fieldState,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FormItemContextValue = {
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
|
||||||
{} as FormItemContextValue
|
|
||||||
)
|
|
||||||
|
|
||||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
const id = React.useId()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormItemContext.Provider value={{ id }}>
|
|
||||||
<div
|
|
||||||
data-slot="form-item"
|
|
||||||
className={cn("grid gap-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</FormItemContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
||||||
const { error, formItemId } = useFormField()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
data-slot="form-label"
|
|
||||||
data-error={!!error}
|
|
||||||
className={cn("data-[error=true]:text-destructive", className)}
|
|
||||||
htmlFor={formItemId}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Slot
|
|
||||||
data-slot="form-control"
|
|
||||||
id={formItemId}
|
|
||||||
aria-describedby={
|
|
||||||
!error
|
|
||||||
? `${formDescriptionId}`
|
|
||||||
: `${formDescriptionId} ${formMessageId}`
|
|
||||||
}
|
|
||||||
aria-invalid={!!error}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
||||||
const { formDescriptionId } = useFormField()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
data-slot="form-description"
|
|
||||||
id={formDescriptionId}
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
|
||||||
const { error, formMessageId } = useFormField()
|
|
||||||
const body = error ? String(error?.message ?? "") : props.children
|
|
||||||
|
|
||||||
if (!body) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
data-slot="form-message"
|
|
||||||
id={formMessageId}
|
|
||||||
className={cn("text-destructive text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{body}
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
useFormField,
|
|
||||||
Form,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormMessage,
|
|
||||||
FormField,
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
|
||||||
|
|
||||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="input-group"
|
|
||||||
role="group"
|
|
||||||
className={cn(
|
|
||||||
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
|
|
||||||
"h-9 min-w-0 has-[>textarea]:h-auto",
|
|
||||||
|
|
||||||
// Variants based on alignment.
|
|
||||||
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
|
||||||
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
|
||||||
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
|
||||||
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
|
||||||
|
|
||||||
// Focus state.
|
|
||||||
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
|
|
||||||
|
|
||||||
// Error state.
|
|
||||||
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
|
||||||
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputGroupAddonVariants = cva(
|
|
||||||
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
align: {
|
|
||||||
"inline-start":
|
|
||||||
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
|
||||||
"inline-end":
|
|
||||||
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
|
||||||
"block-start":
|
|
||||||
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
|
||||||
"block-end":
|
|
||||||
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
align: "inline-start",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function InputGroupAddon({
|
|
||||||
className,
|
|
||||||
align = "inline-start",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
data-slot="input-group-addon"
|
|
||||||
data-align={align}
|
|
||||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
||||||
onClick={(e) => {
|
|
||||||
if ((e.target as HTMLElement).closest("button")) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputGroupButtonVariants = cva(
|
|
||||||
"text-sm shadow-none flex gap-2 items-center",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
size: {
|
|
||||||
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
|
|
||||||
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
|
|
||||||
"icon-xs":
|
|
||||||
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
|
||||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
size: "xs",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function InputGroupButton({
|
|
||||||
className,
|
|
||||||
type = "button",
|
|
||||||
variant = "ghost",
|
|
||||||
size = "xs",
|
|
||||||
...props
|
|
||||||
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
|
||||||
VariantProps<typeof inputGroupButtonVariants>) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
type={type}
|
|
||||||
data-size={size}
|
|
||||||
variant={variant}
|
|
||||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"input">) {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
data-slot="input-group-control"
|
|
||||||
className={cn(
|
|
||||||
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupTextarea({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"textarea">) {
|
|
||||||
return (
|
|
||||||
<Textarea
|
|
||||||
data-slot="input-group-control"
|
|
||||||
className={cn(
|
|
||||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupText,
|
|
||||||
InputGroupInput,
|
|
||||||
InputGroupTextarea,
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={type}
|
|
||||||
data-slot="input"
|
|
||||||
className={cn(
|
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Input }
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
|
||||||
return (
|
|
||||||
<kbd
|
|
||||||
data-slot="kbd"
|
|
||||||
className={cn(
|
|
||||||
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
|
|
||||||
"[&_svg:not([class*='size-'])]:size-3",
|
|
||||||
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<kbd
|
|
||||||
data-slot="kbd-group"
|
|
||||||
className={cn("inline-flex items-center gap-1", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Kbd, KbdGroup }
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Label({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<LabelPrimitive.Root
|
|
||||||
data-slot="label"
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Label }
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Popover({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
|
||||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
|
||||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverContent({
|
|
||||||
className,
|
|
||||||
align = "center",
|
|
||||||
sideOffset = 4,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<PopoverPrimitive.Portal>
|
|
||||||
<PopoverPrimitive.Content
|
|
||||||
data-slot="popover-content"
|
|
||||||
align={align}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</PopoverPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverAnchor({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
|
||||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Progress({
|
|
||||||
className,
|
|
||||||
value,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<ProgressPrimitive.Root
|
|
||||||
data-slot="progress"
|
|
||||||
className={cn(
|
|
||||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ProgressPrimitive.Indicator
|
|
||||||
data-slot="progress-indicator"
|
|
||||||
className="bg-primary h-full w-full flex-1 transition-all"
|
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
||||||
/>
|
|
||||||
</ProgressPrimitive.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Progress }
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
||||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Select({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
|
||||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
|
||||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectValue({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
|
||||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectTrigger({
|
|
||||||
className,
|
|
||||||
size = "default",
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
||||||
size?: "sm" | "default"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Trigger
|
|
||||||
data-slot="select-trigger"
|
|
||||||
data-size={size}
|
|
||||||
className={cn(
|
|
||||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon asChild>
|
|
||||||
<ChevronDownIcon className="size-4 opacity-50" />
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
position = "popper",
|
|
||||||
align = "center",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
data-slot="select-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
|
||||||
position === "popper" &&
|
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
position={position}
|
|
||||||
align={align}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SelectScrollUpButton />
|
|
||||||
<SelectPrimitive.Viewport
|
|
||||||
className={cn(
|
|
||||||
"p-1",
|
|
||||||
position === "popper" &&
|
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
<SelectScrollDownButton />
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Label
|
|
||||||
data-slot="select-label"
|
|
||||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
data-slot="select-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
|
||||||
<SelectPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</SelectPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Separator
|
|
||||||
data-slot="select-separator"
|
|
||||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollUpButton({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
|
||||||
data-slot="select-scroll-up-button"
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronUpIcon className="size-4" />
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollDownButton({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
data-slot="select-scroll-down-button"
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronDownIcon className="size-4" />
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectScrollDownButton,
|
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Separator({
|
|
||||||
className,
|
|
||||||
orientation = "horizontal",
|
|
||||||
decorative = true,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<SeparatorPrimitive.Root
|
|
||||||
data-slot="separator"
|
|
||||||
decorative={decorative}
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Separator }
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="skeleton"
|
|
||||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Skeleton }
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Loader2Icon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
|
||||||
return (
|
|
||||||
<Loader2Icon
|
|
||||||
role="status"
|
|
||||||
aria-label="Loading"
|
|
||||||
className={cn("size-4 animate-spin", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Spinner }
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
||||||
return (
|
|
||||||
<textarea
|
|
||||||
data-slot="textarea"
|
|
||||||
className={cn(
|
|
||||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Textarea }
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { userContext } from '@/context'
|
|
||||||
import { createSessionStorage } from '@/lib/session'
|
|
||||||
|
|
||||||
import { createAuth, type User } from '@/lib/auth'
|
|
||||||
import { decodeJwt } from 'jose'
|
|
||||||
import { redirect, type LoaderFunctionArgs } from 'react-router'
|
|
||||||
import type { OAuth2Strategy } from 'remix-auth-oauth2'
|
|
||||||
|
|
||||||
export const authMiddleware = async (
|
|
||||||
{ request, context }: LoaderFunctionArgs,
|
|
||||||
next
|
|
||||||
): Promise<Response> => {
|
|
||||||
const sessionStorage = createSessionStorage(context.cloudflare.env)
|
|
||||||
const authenticator = createAuth(context.cloudflare.env)
|
|
||||||
const strategy = authenticator.get<OAuth2Strategy<User>>('oidc')
|
|
||||||
const session = await sessionStorage.getSession(request.headers.get('cookie'))
|
|
||||||
let user = session.get('user') as User | null
|
|
||||||
|
|
||||||
session.set('returnTo', new URL(request.url).toString())
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
console.log('There is no user logged in')
|
|
||||||
|
|
||||||
return redirect('/login', {
|
|
||||||
headers: { 'Set-Cookie': await sessionStorage.commitSession(session) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const accessToken = decodeJwt(user.accessToken) as { exp: number }
|
|
||||||
const exp = accessToken.exp * 1000
|
|
||||||
const leeway = 30 * 1000
|
|
||||||
|
|
||||||
if (Date.now() > exp - leeway) {
|
|
||||||
// @ts-ignore
|
|
||||||
const tokens = await strategy.refreshToken(user.refreshToken)
|
|
||||||
|
|
||||||
user = {
|
|
||||||
...user,
|
|
||||||
accessToken: tokens.accessToken(),
|
|
||||||
refreshToken: tokens.refreshToken()
|
|
||||||
}
|
|
||||||
|
|
||||||
session.set('user', user)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
// If refreshing the token fails, remove the user from the current session
|
|
||||||
// so the user is forced to sign in again
|
|
||||||
session.unset('user')
|
|
||||||
|
|
||||||
return redirect('/', {
|
|
||||||
headers: { 'Set-Cookie': await sessionStorage.commitSession(session) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
context.set(userContext, user)
|
|
||||||
|
|
||||||
const response = await next()
|
|
||||||
response.headers.set(
|
|
||||||
'Set-Cookie',
|
|
||||||
await sessionStorage.commitSession(session)
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { Route } from './+types/root'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isRouteErrorResponse,
|
isRouteErrorResponse,
|
||||||
Links,
|
Links,
|
||||||
@@ -7,23 +9,34 @@ import {
|
|||||||
ScrollRestoration
|
ScrollRestoration
|
||||||
} from 'react-router'
|
} from 'react-router'
|
||||||
|
|
||||||
import type { Route } from './+types/root'
|
import { loggingMiddleware } from '@repo/auth/middleware/logging'
|
||||||
|
import { ThemeProvider } from '@repo/ui/components/theme-provider'
|
||||||
import './app.css'
|
import './app.css'
|
||||||
|
|
||||||
|
export const middleware: Route.MiddlewareFunction[] = [loggingMiddleware]
|
||||||
|
|
||||||
export function Layout({ children }: { children: React.ReactNode }) {
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="pt-br" className="dark h-full">
|
<html lang="pt-br" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<title>saladeaula.digital</title>
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
<body className="h-full">
|
<body>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ import type { Route } from './+types'
|
|||||||
|
|
||||||
import { redirect } from 'react-router'
|
import { redirect } from 'react-router'
|
||||||
|
|
||||||
import { createAuth, type User } from '@/lib/auth'
|
import { createAuth, type User } from '@repo/auth/auth'
|
||||||
import { createSessionStorage } from '@/lib/session'
|
import { requestIdContext } from '@repo/auth/context'
|
||||||
|
import { createSessionStorage } from '@repo/auth/session'
|
||||||
|
|
||||||
export async function loader({ request, context }: Route.ActionArgs) {
|
export async function loader({ request, context }: Route.ActionArgs) {
|
||||||
const sessionStorage = createSessionStorage(context.cloudflare.env)
|
const sessionStorage = createSessionStorage(context.cloudflare.env)
|
||||||
const session = await sessionStorage.getSession(request.headers.get('cookie'))
|
const session = await sessionStorage.getSession(request.headers.get('cookie'))
|
||||||
const returnTo = session.has('returnTo') ? session.get('returnTo') : '/'
|
const returnTo = session.has('returnTo') ? session.get('returnTo') : '/'
|
||||||
|
const requestId = context.get(requestIdContext)
|
||||||
const user = session.get('user') as User | null
|
const user = session.get('user') as User | null
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
@@ -20,7 +22,8 @@ export async function loader({ request, context }: Route.ActionArgs) {
|
|||||||
const user = await authenticator.authenticate('oidc', request)
|
const user = await authenticator.authenticate('oidc', request)
|
||||||
session.set('user', user)
|
session.set('user', user)
|
||||||
|
|
||||||
console.log(`Redirecting the user to ${returnTo}`)
|
console.log(`[${requestId}] Redirecting the user to ${returnTo}`)
|
||||||
|
|
||||||
// Redirect to the home page after successful login
|
// Redirect to the home page after successful login
|
||||||
return redirect(returnTo, {
|
return redirect(returnTo, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -28,7 +31,7 @@ export async function loader({ request, context }: Route.ActionArgs) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(`[${requestId}]`, error)
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { Route } from './+types'
|
import type { Route } from './+types'
|
||||||
|
|
||||||
import { createAuth, type User } from '@/lib/auth'
|
|
||||||
import { createSessionStorage } from '@/lib/session'
|
|
||||||
import { redirect } from 'react-router'
|
import { redirect } from 'react-router'
|
||||||
import type { OAuth2Strategy } from 'remix-auth-oauth2'
|
import type { OAuth2Strategy } from 'remix-auth-oauth2'
|
||||||
|
|
||||||
|
import { createAuth, type User } from '@repo/auth/auth'
|
||||||
|
import { createSessionStorage } from '@repo/auth/session'
|
||||||
|
|
||||||
export async function loader({ request, context }: Route.LoaderArgs) {
|
export async function loader({ request, context }: Route.LoaderArgs) {
|
||||||
const authenticator = createAuth(context.cloudflare.env)
|
const authenticator = createAuth(context.cloudflare.env)
|
||||||
const sessionStorage = createSessionStorage(context.cloudflare.env)
|
const sessionStorage = createSessionStorage(context.cloudflare.env)
|
||||||
@@ -17,8 +18,6 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return redirect('/login', {
|
return redirect('/login', {
|
||||||
headers: new Headers({
|
headers: { 'Set-Cookie': await sessionStorage.destroySession(session) }
|
||||||
'Set-Cookie': await sessionStorage.destroySession(session)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,37 +7,40 @@ import {
|
|||||||
EmptyHeader,
|
EmptyHeader,
|
||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
EmptyTitle
|
EmptyTitle
|
||||||
} from '@/components/ui/empty'
|
} from '@repo/ui/components/ui/empty'
|
||||||
import {
|
import {
|
||||||
BanIcon,
|
BanIcon,
|
||||||
|
BookCopyIcon,
|
||||||
CircleCheckIcon,
|
CircleCheckIcon,
|
||||||
CircleIcon,
|
CircleIcon,
|
||||||
CircleOffIcon,
|
CircleOffIcon,
|
||||||
CircleXIcon,
|
CircleXIcon,
|
||||||
HelpCircleIcon,
|
|
||||||
TimerIcon,
|
TimerIcon,
|
||||||
type LucideIcon
|
type LucideIcon
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
// import lzwCompress from 'lzwcompress'
|
// import lzwCompress from 'lzwcompress'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { Suspense, useMemo, useState } from 'react'
|
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
||||||
import { Await, useLoaderData } from 'react-router'
|
import { Suspense, useMemo } from 'react'
|
||||||
|
import { Await, useSearchParams } from 'react-router'
|
||||||
|
|
||||||
import placeholder from '@/assets/placeholder.webp'
|
import placeholder from '@/assets/placeholder.webp'
|
||||||
import { FacetedFilter } from '@/components/faceted-filter'
|
import { createSearch } from '@/lib/meili'
|
||||||
import { SearchForm } from '@/components/search-form'
|
|
||||||
import { Skeleton } from '@/components/skeleton'
|
import type { User } from '@repo/auth/auth'
|
||||||
|
import { userContext } from '@repo/auth/context'
|
||||||
|
import { FacetedFilter } from '@repo/ui/components/faceted-filter'
|
||||||
|
import { SearchForm } from '@repo/ui/components/search-form'
|
||||||
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle
|
CardTitle
|
||||||
} from '@/components/ui/card'
|
} from '@repo/ui/components/ui/card'
|
||||||
import { Progress } from '@/components/ui/progress'
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||||
import { userContext } from '@/context'
|
import { Progress } from '@repo/ui/components/ui/progress'
|
||||||
import type { User } from '@/lib/auth'
|
|
||||||
import { createSearch } from '@/lib/meili'
|
|
||||||
|
|
||||||
export const data = [
|
export const data = [
|
||||||
{
|
{
|
||||||
@@ -72,51 +75,31 @@ export function meta({}: Route.MetaArgs) {
|
|||||||
return [{ title: 'Meus cursos' }]
|
return [{ title: 'Meus cursos' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loader = async ({ context }: Route.ActionArgs) => {
|
export const loader = async ({ request, context }: Route.ActionArgs) => {
|
||||||
const user = context.get(userContext) as User
|
const user = context.get(userContext) as User
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const status = searchParams.getAll('status') || []
|
||||||
|
let builder = new MeiliSearchFilterBuilder().where('user.id', '=', user.sub)
|
||||||
|
|
||||||
|
if (status.length) {
|
||||||
|
builder = builder.where('status', 'in', status)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: createSearch({
|
data: createSearch({
|
||||||
index: 'betaeducacao-prod-enrollments',
|
index: 'betaeducacao-prod-enrollments',
|
||||||
filter: `user.id = "${user.sub}"`,
|
filter: builder.build(),
|
||||||
sort: ['created_at:desc'],
|
sort: ['created_at:desc'],
|
||||||
env: context.cloudflare.env
|
env: context.cloudflare.env
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const statuses = [
|
export default function Component({
|
||||||
{
|
loaderData: { data }
|
||||||
value: 'PENDING',
|
}: Route.ComponentProps) {
|
||||||
label: 'Não iniciado',
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
icon: CircleIcon
|
const term = searchParams.get('term') as string
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'IN_PROGRESS',
|
|
||||||
label: 'Em andamento',
|
|
||||||
icon: TimerIcon
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'COMPLETED',
|
|
||||||
label: 'Aprovado',
|
|
||||||
icon: CircleCheckIcon
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'FAILED',
|
|
||||||
label: 'Reprovado',
|
|
||||||
icon: CircleXIcon
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'CANCELED',
|
|
||||||
label: 'Cancelado',
|
|
||||||
icon: CircleOffIcon
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function Component({}: Route.ComponentProps) {
|
|
||||||
const { data } = useLoaderData()
|
|
||||||
const [term, setTerm] = useState<string>('')
|
|
||||||
const [status, setStatus] = useState<string[]>([])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -133,16 +116,43 @@ export default function Component({}: Route.ComponentProps) {
|
|||||||
<div className="flex gap-2.5">
|
<div className="flex gap-2.5">
|
||||||
<div className="w-full xl:w-93">
|
<div className="w-full xl:w-93">
|
||||||
<SearchForm
|
<SearchForm
|
||||||
onChange={(e) => {
|
defaultValue={searchParams.get('term') || ''}
|
||||||
setTerm(e.target.value)
|
placeholder={
|
||||||
}}
|
<>
|
||||||
|
Pressione <Kbd>/</Kbd> para filtrar...
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onChange={(value) =>
|
||||||
|
setSearchParams((searchParams) => {
|
||||||
|
searchParams.set('term', String(value))
|
||||||
|
return searchParams
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FacetedFilter
|
<FacetedFilter
|
||||||
value={status}
|
icon={BookCopyIcon}
|
||||||
onChange={setStatus}
|
value={searchParams.getAll('status')}
|
||||||
|
onChange={(statuses) => {
|
||||||
|
setSearchParams((searchParams) => {
|
||||||
|
searchParams.delete('status')
|
||||||
|
|
||||||
|
if (statuses.length) {
|
||||||
|
statuses.forEach((s) =>
|
||||||
|
searchParams.has('status', s)
|
||||||
|
? null
|
||||||
|
: searchParams.append('status', s)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchParams
|
||||||
|
})
|
||||||
|
}}
|
||||||
title="Status"
|
title="Status"
|
||||||
options={statuses}
|
options={Object.entries(statuses).map(([key, value]) => ({
|
||||||
|
value: key,
|
||||||
|
...value
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -253,38 +263,32 @@ function Enrollment({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const statusIcon: Record<string, { icon: LucideIcon; color: string }> = {
|
const statuses: Record<
|
||||||
|
string,
|
||||||
|
{ icon: LucideIcon; color?: string; label: string }
|
||||||
|
> = {
|
||||||
PENDING: {
|
PENDING: {
|
||||||
icon: CircleIcon,
|
icon: CircleIcon,
|
||||||
color: 'bg-gray-500/50 border-gray-500'
|
label: 'Não iniciado'
|
||||||
},
|
},
|
||||||
IN_PROGRESS: {
|
IN_PROGRESS: {
|
||||||
icon: TimerIcon,
|
icon: TimerIcon,
|
||||||
color: 'bg-blue-500/50 border-blue-500'
|
color: 'text-blue-400 [&_svg]:text-blue-500',
|
||||||
|
label: 'Em andamento'
|
||||||
},
|
},
|
||||||
COMPLETED: {
|
COMPLETED: {
|
||||||
icon: CircleCheckIcon,
|
icon: CircleCheckIcon,
|
||||||
color: 'bg-green-500/50 border-green-500'
|
color: 'text-green-400 [&_svg]:text-background [&_svg]:fill-green-500',
|
||||||
|
label: 'Aprovado'
|
||||||
},
|
},
|
||||||
FAILED: {
|
FAILED: {
|
||||||
icon: CircleXIcon,
|
icon: CircleXIcon,
|
||||||
color: 'bg-red-500/50 border-red-500'
|
color: 'text-red-400 [&_svg]:text-red-500',
|
||||||
|
label: 'Reprovado'
|
||||||
},
|
},
|
||||||
CANCELED: {
|
CANCELED: {
|
||||||
icon: CircleOffIcon,
|
icon: CircleOffIcon,
|
||||||
color: 'bg-orange-500/50 border-orange-500'
|
color: 'text-orange-400 [&_svg]:text-orange-500',
|
||||||
|
label: 'Cancelado'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultIcon = {
|
|
||||||
icon: HelpCircleIcon,
|
|
||||||
color: 'bg-gray-500 border-gray-500'
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusTranslate: Record<string, string> = {
|
|
||||||
PENDING: 'Não iniciado',
|
|
||||||
IN_PROGRESS: 'Em andamento',
|
|
||||||
COMPLETED: 'Aprovado',
|
|
||||||
FAILED: 'Reprovado',
|
|
||||||
CANCELED: 'Cancelado'
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import type { Route } from './+types'
|
import type { Route } from './+types'
|
||||||
|
|
||||||
import { Link, Outlet } from 'react-router'
|
import { Outlet } from 'react-router'
|
||||||
|
|
||||||
import logo from '@/components/logo.svg'
|
import { userContext } from '@repo/auth/context'
|
||||||
import { NavUser } from '@/components/nav-user'
|
import { authMiddleware } from '@repo/auth/middleware/auth'
|
||||||
import { userContext } from '@/context'
|
|
||||||
import { authMiddleware } from '@/middleware/auth'
|
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
|
||||||
|
import { NavUser } from '@repo/ui/components/nav-user'
|
||||||
|
|
||||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||||
|
|
||||||
@@ -24,11 +25,10 @@ export default function Component({ loaderData }: Route.ComponentProps) {
|
|||||||
px-4 py-2 lg:py-4 sticky top-0 z-5"
|
px-4 py-2 lg:py-4 sticky top-0 z-5"
|
||||||
>
|
>
|
||||||
<div className="container mx-auto flex items-center">
|
<div className="container mx-auto flex items-center">
|
||||||
<Link to="/">
|
<ThemedImage />
|
||||||
<img src={logo} className="h-6 lg:h-8" />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="ml-auto">
|
<div className="ml-auto flex gap-2.5 items-center">
|
||||||
|
<ModeToggle />
|
||||||
<NavUser user={user} />
|
<NavUser user={user} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator
|
BreadcrumbSeparator
|
||||||
} from '@/components/ui/breadcrumb'
|
} from '@repo/ui/components/ui/breadcrumb'
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [{ title: 'Histórico de compras' }]
|
return [{ title: 'Histórico de compras' }]
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import type { Route } from './+types'
|
|||||||
|
|
||||||
import { Link } from 'react-router'
|
import { Link } from 'react-router'
|
||||||
|
|
||||||
|
import { userContext } from '@/context'
|
||||||
|
import { request as req } from '@/lib/request'
|
||||||
|
import type { User } from '@/middleware/auth'
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
@@ -9,15 +12,15 @@ import {
|
|||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator
|
BreadcrumbSeparator
|
||||||
} from '@/components/ui/breadcrumb'
|
} from '@repo/ui/components/ui/breadcrumb'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle
|
CardTitle
|
||||||
} from '@/components/ui/card'
|
} from '@repo/ui/components/ui/card'
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -25,12 +28,9 @@ import {
|
|||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage
|
FormMessage
|
||||||
} from '@/components/ui/form'
|
} from '@repo/ui/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@repo/ui/components/ui/input'
|
||||||
import { Spinner } from '@/components/ui/spinner'
|
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||||
import { userContext } from '@/context'
|
|
||||||
import { request as req } from '@/lib/request'
|
|
||||||
import type { User } from '@/middleware/auth'
|
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
|||||||
@@ -12,42 +12,24 @@
|
|||||||
"typecheck": "npm run cf-typegen && react-router typegen && tsc --noEmit"
|
"typecheck": "npm run cf-typegen && react-router typegen && tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@repo/auth": "*",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@repo/ui": "*",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
|
||||||
"@react-router/dev": "^7.9.5",
|
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"cmdk": "^1.1.1",
|
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
"jose": "^6.1.0",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"lucide-react": "^0.548.0",
|
|
||||||
"lzwcompress": "^1.1.0",
|
"lzwcompress": "^1.1.0",
|
||||||
"meilisearch": "^0.54.0",
|
"meilisearch": "^0.54.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.65.0",
|
|
||||||
"react-router": "^7.9.5",
|
"react-router": "^7.9.5",
|
||||||
"remix-auth-oauth2": "^3.4.1",
|
|
||||||
"remix-themes": "^2.0.4",
|
|
||||||
"rrweb": "^2.0.0-alpha.4",
|
"rrweb": "^2.0.0-alpha.4",
|
||||||
"scorm-again": "^2.6.7",
|
"scorm-again": "^2.6.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@react-router/dev": "^7.9.5",
|
||||||
"@cloudflare/vite-plugin": "^1.13.17",
|
"@cloudflare/vite-plugin": "^1.13.17",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// Generated by Wrangler by running `wrangler types` (hash: e18fc2110ce73cc9f6afb0de66f703db)
|
// Generated by Wrangler by running `wrangler types` (hash: a901705264ceb3e725e174d55b20e764)
|
||||||
// Runtime types generated with workerd@1.20251011.0 2025-04-04 nodejs_compat
|
// Runtime types generated with workerd@1.20251011.0 2025-04-04 nodejs_compat
|
||||||
declare namespace Cloudflare {
|
declare namespace Cloudflare {
|
||||||
interface GlobalProps {
|
interface GlobalProps {
|
||||||
@@ -7,13 +7,16 @@ declare namespace Cloudflare {
|
|||||||
}
|
}
|
||||||
interface Env {
|
interface Env {
|
||||||
CLIENT_ID: "1a5483ab-4521-4702-9115-5857ac676851";
|
CLIENT_ID: "1a5483ab-4521-4702-9115-5857ac676851";
|
||||||
REDIRECT_URI: "https://scorm.eduseg.workers.dev/login";
|
|
||||||
SCOPE: "openid profile email offline_access";
|
SCOPE: "openid profile email offline_access";
|
||||||
API_URL: "https://bcs7fgb9og.execute-api.sa-east-1.amazonaws.com";
|
API_URL: "https://bcs7fgb9og.execute-api.sa-east-1.amazonaws.com";
|
||||||
ISSUER_URL: "https://id.saladeaula.digital";
|
ISSUER_URL: "https://id.saladeaula.digital";
|
||||||
BUCKET_NAME: "saladeaula.digital";
|
BUCKET_NAME: "saladeaula.digital";
|
||||||
BUCKET_ENDPOINT: "https://s3.sa-east-1.amazonaws.com";
|
BUCKET_ENDPOINT: "https://s3.sa-east-1.amazonaws.com";
|
||||||
MEILI_HOST: "https://search.saladeaula.digital";
|
MEILI_HOST: "https://search.saladeaula.digital";
|
||||||
|
CLIENT_SECRET: string;
|
||||||
|
REDIRECT_URI: string;
|
||||||
|
SESSION_SECRET: string;
|
||||||
|
MEILI_API_KEY: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
interface Env extends Cloudflare.Env {}
|
interface Env extends Cloudflare.Env {}
|
||||||
@@ -21,7 +24,7 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
|
|||||||
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
|
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
|
||||||
};
|
};
|
||||||
declare namespace NodeJS {
|
declare namespace NodeJS {
|
||||||
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "CLIENT_ID" | "REDIRECT_URI" | "SCOPE" | "API_URL" | "ISSUER_URL" | "BUCKET_NAME" | "BUCKET_ENDPOINT" | "MEILI_HOST">> {}
|
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "CLIENT_ID" | "SCOPE" | "API_URL" | "ISSUER_URL" | "BUCKET_NAME" | "BUCKET_ENDPOINT" | "MEILI_HOST" | "CLIENT_SECRET" | "REDIRECT_URI" | "SESSION_SECRET" | "MEILI_API_KEY">> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin runtime types
|
// Begin runtime types
|
||||||
|
|||||||
102
package-lock.json
generated
102
package-lock.json
generated
@@ -25,26 +25,20 @@
|
|||||||
"apps/admin.saladeaula.digital": {
|
"apps/admin.saladeaula.digital": {
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@brazilian-utils/brazilian-utils": "^1.0.0-rc.12",
|
|
||||||
"@hookform/resolvers": "^5.2.2",
|
|
||||||
"@react-router/fs-routes": "^7.9.5",
|
"@react-router/fs-routes": "^7.9.5",
|
||||||
|
"@repo/auth": "*",
|
||||||
"@repo/ui": "*",
|
"@repo/ui": "*",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"http-status-codes": "^2.3.0",
|
"http-status-codes": "^2.3.0",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
"jose": "^6.1.0",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"meilisearch": "^0.54.0",
|
"meilisearch": "^0.54.0",
|
||||||
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.66.0",
|
|
||||||
"react-number-format": "^5.4.4",
|
|
||||||
"react-router": "^7.9.5",
|
"react-router": "^7.9.5",
|
||||||
"remix-auth-oauth2": "^3.4.1",
|
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -164,43 +158,25 @@
|
|||||||
"apps/saladeaula.digital": {
|
"apps/saladeaula.digital": {
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@repo/auth": "*",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@repo/ui": "*",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
|
||||||
"@react-router/dev": "^7.9.5",
|
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"cmdk": "^1.1.1",
|
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
"jose": "^6.1.0",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"lucide-react": "^0.548.0",
|
|
||||||
"lzwcompress": "^1.1.0",
|
"lzwcompress": "^1.1.0",
|
||||||
"meilisearch": "^0.54.0",
|
"meilisearch": "^0.54.0",
|
||||||
|
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.65.0",
|
|
||||||
"react-router": "^7.9.5",
|
"react-router": "^7.9.5",
|
||||||
"remix-auth-oauth2": "^3.4.1",
|
|
||||||
"remix-themes": "^2.0.4",
|
|
||||||
"rrweb": "^2.0.0-alpha.4",
|
"rrweb": "^2.0.0-alpha.4",
|
||||||
"scorm-again": "^2.6.7",
|
"scorm-again": "^2.6.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/vite-plugin": "^1.13.17",
|
"@cloudflare/vite-plugin": "^1.13.17",
|
||||||
|
"@react-router/dev": "^7.9.5",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/node": "^24",
|
"@types/node": "^24",
|
||||||
@@ -3961,6 +3937,10 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@repo/auth": {
|
||||||
|
"resolved": "packages/auth",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@repo/ui": {
|
"node_modules/@repo/ui": {
|
||||||
"resolved": "packages/ui",
|
"resolved": "packages/ui",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -4605,6 +4585,13 @@
|
|||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
|
||||||
|
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/luxon": {
|
"node_modules/@types/luxon": {
|
||||||
"version": "3.7.1",
|
"version": "3.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
|
||||||
@@ -6422,19 +6409,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/remix-themes": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/remix-themes/-/remix-themes-2.0.4.tgz",
|
|
||||||
"integrity": "sha512-S1vIx86xdsMv+ceaWGWtlVZBMhU4tmZeBOBzOiZnhbHnGBbd5YeyIlZ5EL3BSEhZq35oEcINeTrMrVju9GPYEA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"workspaces": [
|
|
||||||
"test-apps/*",
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"peerDependencies": {
|
|
||||||
"react-router": ">=7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/retry": {
|
"node_modules/retry": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||||
@@ -7155,9 +7129,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.12",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.0.tgz",
|
||||||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
"integrity": "sha512-C/Naxf8H0pBx1PA4BdpT+c/5wdqI9ILMdwjSMILw7tVIh3JsxzZqdeTLmmdaoh5MYUEOyBnM9K3o0DzoZ/fe+w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
@@ -7966,10 +7940,40 @@
|
|||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/auth": {
|
||||||
|
"name": "@repo/auth",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"jose": "^6.1.0",
|
||||||
|
"remix-auth-oauth2": "^3.4.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.9.2",
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"@types/react-dom": "^19.2.2",
|
||||||
|
"react-router": "^7.9.5",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/auth/node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@repo/ui",
|
"name": "@repo/ui",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@brazilian-utils/brazilian-utils": "^1.0.0-rc.12",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
@@ -7989,15 +7993,23 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.548.0",
|
"lucide-react": "^0.548.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-hook-form": "^7.66.0",
|
"react-hook-form": "^7.66.0",
|
||||||
|
"react-number-format": "^5.4.4",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/lodash": "^4.17.20",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
"vite": "^7.2.0",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
packages/auth/package.json
Normal file
22
packages/auth/package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/auth",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"exports": {
|
||||||
|
"./auth": "./src/auth.ts",
|
||||||
|
"./session": "./src/session.ts",
|
||||||
|
"./context": "./src/context.ts",
|
||||||
|
"./middleware/*": "./src/middleware/*.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"jose": "^6.1.0",
|
||||||
|
"remix-auth-oauth2": "^3.4.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"react-router": "^7.9.5",
|
||||||
|
"@types/node": "^24.9.2",
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"@types/react-dom": "^19.2.2",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ export type User = {
|
|||||||
refreshToken: string
|
refreshToken: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAuth(env: Env) {
|
export function createAuth(env) {
|
||||||
const authenticator = new Authenticator()
|
const authenticator = new Authenticator()
|
||||||
const strategy = new OAuth2Strategy(
|
const strategy = new OAuth2Strategy(
|
||||||
{
|
{
|
||||||
5
packages/auth/src/context.ts
Normal file
5
packages/auth/src/context.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { User } from '@/auth'
|
||||||
|
import { createContext } from 'react-router'
|
||||||
|
|
||||||
|
export const userContext = createContext<User | null>(null)
|
||||||
|
export const requestIdContext = createContext<string | null>(null)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { requestIdContext, userContext } from '@/context'
|
import { requestIdContext, userContext } from '@/context'
|
||||||
import { createSessionStorage } from '@/lib/session'
|
import { createSessionStorage } from '@/session'
|
||||||
|
|
||||||
import { createAuth, type User } from '@/lib/auth'
|
import { createAuth, type User } from '@/auth'
|
||||||
import { decodeJwt } from 'jose'
|
import { decodeJwt } from 'jose'
|
||||||
import { redirect, type LoaderFunctionArgs } from 'react-router'
|
import { redirect, type LoaderFunctionArgs } from 'react-router'
|
||||||
import type { OAuth2Strategy } from 'remix-auth-oauth2'
|
import type { OAuth2Strategy } from 'remix-auth-oauth2'
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createCookieSessionStorage } from 'react-router'
|
import { createCookieSessionStorage } from 'react-router'
|
||||||
|
|
||||||
export function createSessionStorage(env: Env) {
|
export function createSessionStorage(env) {
|
||||||
const sessionStorage = createCookieSessionStorage({
|
const sessionStorage = createCookieSessionStorage({
|
||||||
cookie: {
|
cookie: {
|
||||||
name: '__session',
|
name: '__session',
|
||||||
11
packages/auth/tsconfig.json
Normal file
11
packages/auth/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -4,8 +4,6 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"exports": {
|
"exports": {
|
||||||
"./globals.css": "./src/globals.css",
|
"./globals.css": "./src/globals.css",
|
||||||
"./postcss.config": "./postcss.config.js",
|
|
||||||
"./tailwind.config": "./tailwind.config.ts",
|
|
||||||
"./lib/*": "./src/lib/*.ts",
|
"./lib/*": "./src/lib/*.ts",
|
||||||
"./hooks/*": [
|
"./hooks/*": [
|
||||||
"./src/hooks/*.ts",
|
"./src/hooks/*.ts",
|
||||||
@@ -15,6 +13,7 @@
|
|||||||
"./components/*.svg": "./src/components/*.svg"
|
"./components/*.svg": "./src/components/*.svg"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@brazilian-utils/brazilian-utils": "^1.0.0-rc.12",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
@@ -34,14 +33,22 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.548.0",
|
"lucide-react": "^0.548.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-hook-form": "^7.66.0",
|
"react-hook-form": "^7.66.0",
|
||||||
|
"react-number-format": "^5.4.4",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/lodash": "^4.17.20",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
"vite": "^7.2.0",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
import { Moon, Sun, SunMoon } from 'lucide-react'
|
import { Moon, Sun, SunMoon } from 'lucide-react'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
|
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
import dark from './logo-dark.svg'
|
||||||
|
import light from './logo-light.svg'
|
||||||
|
import { Button } from './ui/button'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@repo/ui/components/ui/dropdown-menu'
|
} from './ui/dropdown-menu'
|
||||||
import dark from './logo-dark.svg'
|
|
||||||
import light from './logo-light.svg'
|
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const { setTheme } = useTheme()
|
const { setTheme } = useTheme()
|
||||||
@@ -4,12 +4,16 @@ import {
|
|||||||
CirclePlayIcon,
|
CirclePlayIcon,
|
||||||
DollarSignIcon,
|
DollarSignIcon,
|
||||||
GraduationCapIcon,
|
GraduationCapIcon,
|
||||||
|
LayoutDashboardIcon,
|
||||||
|
LightbulbIcon,
|
||||||
LogOutIcon,
|
LogOutIcon,
|
||||||
UserIcon
|
UserIcon,
|
||||||
|
type LucideIcon
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Link } from 'react-router'
|
import { Link } from 'react-router'
|
||||||
|
|
||||||
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
|
import { initials } from '../lib/utils'
|
||||||
|
import { Avatar, AvatarFallback } from './ui/avatar'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -18,8 +22,40 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@repo/ui/components/ui/dropdown-menu'
|
} from './ui/dropdown-menu'
|
||||||
import { initials } from '@repo/ui/lib/utils'
|
|
||||||
|
type NavItem = {
|
||||||
|
title: string
|
||||||
|
url: string
|
||||||
|
icon: LucideIcon
|
||||||
|
scope?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const apps: NavItem[] = [
|
||||||
|
{
|
||||||
|
title: 'Sala de aula',
|
||||||
|
url: '//scorm.eduseg.workers.dev',
|
||||||
|
icon: GraduationCapIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Administrador',
|
||||||
|
url: '//admin.saladeaula.digital',
|
||||||
|
icon: LayoutDashboardIcon,
|
||||||
|
scope: ['apps:admin']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'EDUSEG® Estúdio',
|
||||||
|
url: '//studio.saladeaula.digital',
|
||||||
|
icon: CirclePlayIcon,
|
||||||
|
scope: ['apps:studio']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'EDUSEG® Insights',
|
||||||
|
url: '//insights.saladeaula.digital',
|
||||||
|
icon: LightbulbIcon,
|
||||||
|
scope: ['apps:insights']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
export function NavUser({
|
export function NavUser({
|
||||||
user
|
user
|
||||||
@@ -30,7 +66,7 @@ export function NavUser({
|
|||||||
scope: string
|
scope: string
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
const scopes = user.scope.split(' ')
|
const userScope = user.scope.split(' ')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -82,7 +118,11 @@ export function NavUser({
|
|||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
|
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
{grantIfHas(['apps:admin', 'apps:studio'], scopes, 'any') && (
|
{grantIfHas(
|
||||||
|
['apps:admin', 'apps:studio', 'apps:insights'],
|
||||||
|
userScope,
|
||||||
|
'any'
|
||||||
|
) && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuLabel className="text-muted-foreground text-sm">
|
<DropdownMenuLabel className="text-muted-foreground text-sm">
|
||||||
@@ -91,26 +131,19 @@ export function NavUser({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DropdownMenuItem asChild>
|
{apps.map(({ title, url, scope = [], icon: Icon }, idx) => {
|
||||||
<Link to="//scorm.eduseg.workers.dev" className="cursor-pointer">
|
if (grantIfHas(scope, userScope)) {
|
||||||
<GraduationCapIcon />
|
return (
|
||||||
Sala de aula
|
<DropdownMenuItem key={idx} asChild>
|
||||||
|
<Link to={url} className="cursor-pointer">
|
||||||
|
<Icon /> {title}
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{grantIfHas(['apps:studio'], scopes) && (
|
return <></>
|
||||||
<>
|
})}
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Link
|
|
||||||
to="//studio.saladeaula.digital"
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<CirclePlayIcon />
|
|
||||||
EDUSEG® Estúdio
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
InputGroupInput
|
InputGroupInput
|
||||||
} from '@repo/ui/components/ui/input-group'
|
} from '@repo/ui/components/ui/input-group'
|
||||||
|
|
||||||
import { useKeyPress } from '@/hooks/use-keypress'
|
import { useKeyPress } from '@repo/ui/hooks/use-keypress'
|
||||||
import { cn } from '@repo/ui/lib/utils'
|
import { cn } from '@repo/ui/lib/utils'
|
||||||
|
|
||||||
export function SearchForm({
|
export function SearchForm({
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Skeleton as XSkeleton } from '@/components/ui/skeleton'
|
import { Skeleton as XSkeleton } from './ui/skeleton'
|
||||||
|
|
||||||
export function Skeleton() {
|
export function Skeleton() {
|
||||||
return (
|
return (
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
|
|
||||||
const MOBILE_BREAKPOINT = 768
|
const MOBILE_BREAKPOINT = 768
|
||||||
|
|
||||||
@@ -10,9 +10,9 @@ export function useIsMobile() {
|
|||||||
const onChange = () => {
|
const onChange = () => {
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||||
}
|
}
|
||||||
mql.addEventListener("change", onChange)
|
mql.addEventListener('change', onChange)
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||||
return () => mql.removeEventListener("change", onChange)
|
return () => mql.removeEventListener('change', onChange)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return !!isMobile
|
return !!isMobile
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"strict": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tailwindcss()]
|
plugins: [tailwindcss(), tsconfigPaths()]
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user