Compare commits

...

3 Commits

Author SHA1 Message Date
14c450d9f0 Updated sl and en translations 2025-09-23 23:47:31 +02:00
ce0ad656e7 Updated packages 2025-09-07 18:24:40 +02:00
cd020e3eb8 Redesigned UI
Fixed SQL query issues with server
2025-09-06 14:27:54 +02:00
13 changed files with 4027 additions and 2615 deletions

2662
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,8 +15,12 @@
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"firebase": "^12.2.1", "firebase": "^12.2.1",
"framer-motion": "^12.23.12", "framer-motion": "^12.23.12",
"i18next": "^25.5.2",
"next-themes": "^0.4.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"react-i18next": "^15.7.3",
"react-icons": "^5.5.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0", "@eslint/js": "^9.19.0",

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,14 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import {
Box,
Button,
VStack,
HStack,
Input,
Text,
Heading,
Flex
} from '@chakra-ui/react';
import { signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth'; import { signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth';
import { auth, googleProvider } from '../firebaseConfig'; import { auth, googleProvider } from '../firebaseConfig';
@@ -36,100 +46,107 @@ const Login = () => {
}; };
return ( return (
<div style={{ maxWidth: '400px', margin: '100px auto', padding: '20px' }}> <Flex
<h2>Login to DZTPS</h2> minH="100vh"
align="center"
{error && ( justify="center"
<div style={{ bg="gray.50"
color: 'red', px={6}
marginBottom: '10px', >
padding: '10px', <Box
border: '1px solid red', maxW="400px"
borderRadius: '4px', w="full"
backgroundColor: '#ffe6e6' bg="white"
}}> boxShadow="2xl"
{error} rounded="xl"
</div> p={8}
)} borderWidth="1px"
borderColor="gray.200"
<form onSubmit={handleEmailLogin}>
<div style={{ marginBottom: '15px' }}>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={loading}
style={{
width: '100%',
padding: '8px',
marginTop: '5px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={loading}
style={{
width: '100%',
padding: '8px',
marginTop: '5px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
/>
</div>
<button
type="submit"
disabled={loading}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.6 : 1
}}
>
{loading ? 'Signing in...' : 'Sign in with Email'}
</button>
</form>
<div style={{ margin: '20px 0', textAlign: 'center' }}>
<span>OR</span>
</div>
<button
onClick={handleGoogleLogin}
disabled={loading}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#db4437',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.6 : 1
}}
> >
{loading ? 'Signing in...' : 'Sign in with Google'} <VStack gap={6}>
</button> <Heading
</div> size="lg"
textAlign="center"
color="gray.700"
>
Login to DZTPS
</Heading>
{error && (
<Box
p={3}
bg="red.50"
border="1px"
borderColor="red.200"
borderRadius="md"
color="red.800"
w="full"
>
{error}
</Box>
)}
<Box as="form" onSubmit={handleEmailLogin} w="full">
<VStack gap={4}>
<Box w="full">
<Text mb={2} fontSize="sm" fontWeight="medium">Email</Text>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={loading}
placeholder="Enter your email"
bg="white"
required
/>
</Box>
<Box w="full">
<Text mb={2} fontSize="sm" fontWeight="medium">Password</Text>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
placeholder="Enter your password"
bg="white"
required
/>
</Box>
<Button
type="submit"
colorPalette="blue"
size="lg"
w="full"
disabled={loading}
>
{loading ? 'Signing in...' : 'Sign in with Email'}
</Button>
</VStack>
</Box>
<HStack w="full">
<Box flex="1" h="1px" bg="gray.200" />
<Text fontSize="sm" px={3} color="gray.500">
OR
</Text>
<Box flex="1" h="1px" bg="gray.200" />
</HStack>
<Button
onClick={handleGoogleLogin}
colorPalette="red"
variant="outline"
size="lg"
w="full"
disabled={loading}
>
{loading ? 'Signing in...' : 'Sign in with Google'}
</Button>
</VStack>
</Box>
</Flex>
); );
}; };

View File

@@ -0,0 +1,90 @@
'use client'
import { ClientOnly, IconButton, Skeleton, Span } from '@chakra-ui/react'
import { ThemeProvider, useTheme } from 'next-themes'
import * as React from 'react'
import { LuMoon, LuSun } from 'react-icons/lu'
export function ColorModeProvider(props) {
return (
<ThemeProvider attribute='class' disableTransitionOnChange {...props} />
)
}
export function useColorMode() {
const { resolvedTheme, setTheme, forcedTheme } = useTheme()
const colorMode = forcedTheme || resolvedTheme
const toggleColorMode = () => {
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
}
return {
colorMode: colorMode,
setColorMode: setTheme,
toggleColorMode,
}
}
export function useColorModeValue(light, dark) {
const { colorMode } = useColorMode()
return colorMode === 'dark' ? dark : light
}
export function ColorModeIcon() {
const { colorMode } = useColorMode()
return colorMode === 'dark' ? <LuMoon /> : <LuSun />
}
export const ColorModeButton = React.forwardRef(
function ColorModeButton(props, ref) {
const { toggleColorMode } = useColorMode()
return (
<ClientOnly fallback={<Skeleton boxSize='8' />}>
<IconButton
onClick={toggleColorMode}
variant='ghost'
aria-label='Toggle color mode'
size='sm'
ref={ref}
{...props}
css={{
_icon: {
width: '5',
height: '5',
},
}}
>
<ColorModeIcon />
</IconButton>
</ClientOnly>
)
},
)
export const LightMode = React.forwardRef(function LightMode(props, ref) {
return (
<Span
color='fg'
display='contents'
className='chakra-theme light'
colorPalette='gray'
colorScheme='light'
ref={ref}
{...props}
/>
)
})
export const DarkMode = React.forwardRef(function DarkMode(props, ref) {
return (
<Span
color='fg'
display='contents'
className='chakra-theme dark'
colorPalette='gray'
colorScheme='dark'
ref={ref}
{...props}
/>
)
})

View File

@@ -0,0 +1,12 @@
'use client'
import { ChakraProvider, defaultSystem } from '@chakra-ui/react'
import { ColorModeProvider } from './color-mode'
export function Provider(props) {
return (
<ChakraProvider value={defaultSystem}>
<ColorModeProvider {...props} />
</ChakraProvider>
)
}

View File

@@ -0,0 +1,43 @@
'use client'
import {
Toaster as ChakraToaster,
Portal,
Spinner,
Stack,
Toast,
createToaster,
} from '@chakra-ui/react'
export const toaster = createToaster({
placement: 'bottom-end',
pauseOnPageIdle: true,
})
export const Toaster = () => {
return (
<Portal>
<ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}>
{(toast) => (
<Toast.Root width={{ md: 'sm' }}>
{toast.type === 'loading' ? (
<Spinner size='sm' color='blue.solid' />
) : (
<Toast.Indicator />
)}
<Stack gap='1' flex='1' maxWidth='100%'>
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
{toast.description && (
<Toast.Description>{toast.description}</Toast.Description>
)}
</Stack>
{toast.action && (
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
)}
{toast.closable && <Toast.CloseTrigger />}
</Toast.Root>
)}
</ChakraToaster>
</Portal>
)
}

View File

@@ -0,0 +1,35 @@
import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react'
import * as React from 'react'
export const Tooltip = React.forwardRef(function Tooltip(props, ref) {
const {
showArrow,
children,
disabled,
portalled = true,
content,
contentProps,
portalRef,
...rest
} = props
if (disabled) return children
return (
<ChakraTooltip.Root {...rest}>
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
<Portal disabled={!portalled} container={portalRef}>
<ChakraTooltip.Positioner>
<ChakraTooltip.Content ref={ref} {...contentProps}>
{showArrow && (
<ChakraTooltip.Arrow>
<ChakraTooltip.ArrowTip />
</ChakraTooltip.Arrow>
)}
{content}
</ChakraTooltip.Content>
</ChakraTooltip.Positioner>
</Portal>
</ChakraTooltip.Root>
)
})

382
client/src/i18ns.js Normal file
View File

@@ -0,0 +1,382 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
i18n
.use(initReactI18next)
.init({
lng: 'sl', // default language
fallbackLng: 'sl',
debug: true,
interpolation: {
escapeValue: false,
},
resources: {
sl: {
translation: {
appName: "DZTPS Urejevalnik članov",
welcome: "Pozdravljeni, ",
logout: "Odjava",
membersManagement: "Urejanje članov",
navigation: {
home: "Člani",
languages: "Jeziki",
fields: "Strokovna področja",
roles: "Vloge",
},
actions: "Možnosti",
viewDetails: "Ogled podrobnosti",
edit: "Urejanje",
cancel: "Prekliči",
delete: "Brisanje",
errorTranslationSaving: "Pri shranjevanju je prišlo do napake",
errorTranslationDeleting: "Pri brisanju je prišlo do napake",
deleteTranslationConfirm: "Ste prepričani, da želite izbrisati prevod?",
yes: "Da",
no: "Ne",
userAdd: {
addNew: "Dodaj novega člana",
update: "Posodobi člana",
},
userEdit: {
enterID: "Vnesi številko izkaznice",
enterFirstName: "Vnesi ime",
enterLastName: "Vnesi priimek",
selectGender: "Izberi spol",
enterBirthPlace: "Vnesi kraj rojstva",
enterNationality: "Vnesi državljanstvo",
enterEducation: "Vnesi šolsko izobrazbo",
enterHomePhoneNumber: "Vnesi domači telefon",
enterWorkPhoneNumber: "Vnesi službeno telefonsko številko",
enterMobilePhoneNumber: "Vnesi mobilno telefonsko številko",
enterEmail: "Vnesi e-poštni naslov",
enterWebsite: "Vnesi URL spletne strani",
enterResidentialAddress: "Naslov",
enterStreet: "Vnesi ulico",
enterPostalCode: "Vnesi poštno številko",
enterPostOffice: "Vnesi pošto",
enterBasicProfession: "Vnesi osnovni poklic",
enterCurrentEmployment: "Vnesi sedanjo zaposlitev",
enterTranslationExperience: "Vnesi prevajalsko prakso",
enterNativeLanguage: "Vnesi materni jezik",
enterHowForeignLanguageWasAcquired: "Vnesi način pridobivanja znanja tujih jezikov",
basicInformation: "Osnovne informacije",
id: "Številka izkaznice:",
gender: "Spol:",
birthDate: "Datum rojstva:",
birthPlace: "Kraj rojstva:",
nationality: "Državljanstvo:",
nativeLanguage: "Materni jezik:",
address: "Naslov:",
street: "Ulica:",
postCode: "Poštna številka:",
postOffice: "Pošta:",
email: "E-pošta:",
homePhone: "Domači telefon:",
workPhone: "Službeni telefon:",
mobilePhone: "Mobilni telefon:",
website: "Spletna stran:",
education: "Šolska izobrazba:",
basicProfession: "Osnovni poklic:",
currentEmployment: "Sedanja zaposlitev:",
translationExperience: "Prevajalska praksa:",
joinDate: "Datum včlanitve:",
directoryListed: "Objava v iskalniku:",
languageKnowledgeAcquisition: "Način pridobivanja znanja tujih jezikov:",
translationSkills: "Prevajanje iz jezika v jezik:",
licensed: "Licenca DZTPS:",
rank: "Rang",
name: "Ime:",
lastname: "Priimek:",
},
translations: {
addNew: "Dodaj nov prevod",
title: "Member Translations",
update: "Posodobi prevod",
from: "Iz",
to: "V",
rank: "Rang",
license: "Licenca DZTPS",
fields: "Strokovna področja",
roles: "Vloga",
fromLanguage: "Iz jezika",
toLanguage: "V jezik",
licenceDZTPS: "Licenca DZTS",
selectSourceLanguage: "Izberi jezik",
selectTargetLanguage: "Izberi jezik",
enterRank: "Vnesi rank",
},
languageAdd: {
addNew: "Dodaj nov jezik",
},
fieldAdd: {
addNew: "Dodaj novo strokovno področje",
},
roleAdd: {
addNew: "Dodaj novo vlogo",
},
languages: {
languagesManagement: "Urejanje jezikov",
id: "ID",
language: "Jezik:",
addLanguage: "Dodaj jezik",
updateLanguage: "Posodobi jezik",
deleteLanguage: "Izbriši jezik",
},
fields: {
fieldsManagement: "Urejanje strokovnih področij",
id: "ID",
field: "Strokovno področje:",
addField: "Dodaj novo strokovno področje",
addNewButton: "Dodaj področje",
updateButton: "Posodobi področje",
},
roles: {
rolesManagement: "Urejanje vlog",
id: "ID",
role: "Vloga:",
enterRoleName: "Vnesi ime vloge",
enterFemaleForm: "Vnesi žensko obliko",
enterMaleForm: "Vnesi moško obliko",
femaleForm: "Izpis (ženski):",
maleForm: "Izpis (moški):",
addRole: "Dodaj novo vlogo",
updateRole: "Posodobi vlogo",
},
views: {
user: {
basicInformation: "Osnovne informacije",
id: "Številka izkaznice",
gender: "Spol",
male: "Moški",
female: "Ženski",
birthDate: "Datum rojstva",
birthPlace: "Kraj rojstva",
nationality: "Državljanstvo",
nativeLanguage: "Materni jezik",
address: "Naslov",
street: "Ulica",
postCode: "Poštna številka",
postOffice: "Pošta",
contactInformation: "Kontaktne informacije",
email: "E-pošta",
homePhone: "Domači telefon",
workPhone: "Službeni telefon",
mobilePhone: "Mobilni telefon",
website: "Spletna stran",
professionalInformation: "Strokovni podatki",
education: "Šolska izobrazba",
basicProfession: "Osnovni poklic",
currentEmployment: "Sedanja zaposlitev",
translationExperience: "Prevajalska praksa",
joinDate: "Datum včlanitve",
directoryListed: "Objava v iskalniku",
languageKnowledgeAcquisition: "Način pridobivanja znanja tujih jezikov",
translationSkills: "Prevajanje iz jezika v jezik",
licensed: "Licenca DZTPS",
notLicensed: "Brez licence DZTPS",
rank: "Rang",
fields: "Strokovna področja",
roles: "Vloge",
name: "Ime",
lastname: "Priimek",
},
language: {
language: "Jezik",
},
fields: {
field: "Ime področja",
},
roles: {
role: "Vloga",
femaleForm: "Ženska oblika",
maleForm: "Moška oblika",
},
}
}
},
en: {
translation: {
appName: "DZTPS Users Management",
welcome: "Welcome, ",
logout: "Logout",
membersManagement: "Members Management",
navigation: {
home: "Members",
languages: "Languages",
fields: "Fields",
roles: "Roles",
},
actions: "Actions",
viewDetails: "View Details",
edit: "Edit",
cancel: "Cancel",
delete: "Delete",
errorTranslationSaving: "Error saving translation",
errorTranslationDeleting: "Error deleting translation",
deleteTranslationConfirm: "Are you sure you want to delete this translation?",
yes: "Yes",
no: "No",
userAdd: {
addNew: "Add new Member",
update: "Update Member",
},
userEdit: {
enterID: "Enter ID number",
enterFirstName: "Enter First Name",
enterLastName: "Enter Last Name",
selectGender: "Select Gender",
enterBirthPlace: "Enter Birthplace",
enterNationality: "Enter Nationality",
enterEducation: "Enter Education",
enterHomePhoneNumber: "Enter Home Phone Number",
enterWorkPhoneNumber: "Enter Work Phone Number",
enterMobilePhoneNumber: "Enter Mobile Phone Number",
enterEmail: "Enter Email address",
enterWebsite: "Enter Website URL",
enterResidentialAddress: "Enter Residential Address",
enterStreet: "Enter Street Address",
enterPostalCode: "Enter Postal Code",
enterPostOffice: "Enter Post Office",
enterBasicProfession: "Enter Basic Profession",
enterCurrentEmployment: "Enter Current Employment",
enterTranslationExperience: "Enter Translation Experience",
enterNativeLanguage: "Enter Native Language",
enterHowForeignLanguageWasAcquired: "Enter how foreign language was acquired",
id: "ID:",
gender: "Gender:",
birthDate: "Birth Date:",
birthPlace: "Birth Place:",
nationality: "Nationality:",
nativeLanguage: "Native Language:",
address: "Address",
street: "Street:",
postCode: "Post Code:",
postOffice: "Post Office:",
email: "Email:",
homePhone: "Home Phone_",
workPhone: "Work Phone:",
mobilePhone: "Mobile Phone:",
website: "Website:",
education: "Education:",
basicProfession: "Basic Profession:",
currentEmployment: "Current Employment:",
translationExperience: "Translation Experience:",
joinDate: "Join Date:",
directoryListed: "Listed in directory:",
languageKnowledgeAcquisition: "Language Knowledge Acquisition:",
rank: "Rank:",
name: "Name:",
lastname: "Last name:",
},
translations: {
addNew: "Add new Translation",
title: "Member Translations",
update: "Update Translation",
from: "From",
to: "To",
rank: "Rank",
license: "License",
fields: "Fields",
roles: "Roles",
fromLanguage: "From Language",
toLanguage: "To Language",
licenceDZTPS: "Licenca DZTS",
selectSourceLanguage: "Select Source Language",
selectTargetLanguage: "Select Target Language",
enterRank: "Enter Rank",
},
languageAdd: {
addNew: "Add new Language",
},
fieldAdd: {
addNew: "Add new Field",
},
roleAdd: {
addNew: "Add new Role",
},
languages: {
languagesManagement: "Languages Management",
id: "ID",
language: "Language:",
addLanguage: "Add language",
updateLanguage: "Update language",
deleteLanguage: "Delete language",
},
fields: {
fieldsManagement: "Field Management",
id: "ID",
field: "Field:",
addField: "Add new field",
addNewButton: "Add field",
updateButton: "Update field",
},
roles: {
rolesManagement: "Roles Management",
id: "ID",
role: "Role:",
enterRoleName: "Enter role name",
enterFemaleForm: "enter female form",
enterMaleForm: "Enter male form",
femaleForm: "Female form:",
maleForm: "Male form:",
addRole: "Add new role",
updateRole: "Update role",
},
views: {
user: {
basicInformation: "Basic Information",
id: "ID",
gender: "Gender",
male: "Male",
female: "Female",
birthDate: "Birth Date",
birthPlace: "Birth Place",
nationality: "Nationality",
nativeLanguage: "Native Language",
address: "Address",
street: "Street",
postCode: "Post Code",
postOffice: "Post Office",
contactInformation: "Contact Information",
email: "Email",
homePhone: "Home Phone",
workPhone: "Work Phone",
mobilePhone: "Mobile Phone",
website: "Website",
professionalInformation: "Professional Information",
education: "Education",
basicProfession: "Basic Profession",
currentEmployment: "Current Employment",
translationExperience: "Translation Experience",
joinDate: "Join Date",
directoryListed: "Listed in directory",
languageKnowledgeAcquisition: "Language Knowledge Acquisition",
translationSkills: "Translation Skills",
licensed: "Licensed",
notLicensed: "Not Licensed",
rank: "Rank",
fields: "Fields",
roles: "Roles",
name: "Name",
lastname: "Last name",
},
language: {
language: "Language",
},
fields: {
field: "Field name",
},
roles: {
role: "Role",
femaleForm: "Female Form",
maleForm: "Male Form",
},
}
}
},
}
});
export default i18n;

View File

@@ -3,9 +3,9 @@
line-height: 1.5; line-height: 1.5;
font-weight: 400; font-weight: 400;
color-scheme: light dark; color-scheme: light;
color: rgba(255, 255, 255, 0.87); color: #213547;
background-color: #242424; background-color: #ffffff;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
@@ -24,8 +24,6 @@ a:hover {
body { body {
margin: 0; margin: 0;
display: flex;
place-items: center;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
} }
@@ -42,7 +40,8 @@ button {
font-size: 1em; font-size: 1em;
font-weight: 500; font-weight: 500;
font-family: inherit; font-family: inherit;
background-color: #1a1a1a; background-color: #f9f9f9;
color: #213547;
cursor: pointer; cursor: pointer;
transition: border-color 0.25s; transition: border-color 0.25s;
} }
@@ -54,15 +53,3 @@ button:focus-visible {
outline: 4px auto -webkit-focus-ring-color; outline: 4px auto -webkit-focus-ring-color;
} }
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -1,10 +1,18 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { ChakraProvider, createSystem, defaultConfig } from "@chakra-ui/react"
import './index.css' import './index.css'
import App from './App.jsx' import App from './App.jsx'
const system = createSystem(defaultConfig)
// import i18n
import "./i18ns";
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<StrictMode> <StrictMode>
<App /> <ChakraProvider value={system}>
<App />
</ChakraProvider>
</StrictMode>, </StrictMode>,
) )

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"]
}
}
}

File diff suppressed because it is too large Load Diff