Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 278 additions & 0 deletions src/modules/ZachetCard/ZachetCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
<script setup lang="ts">
import { watch } from 'vue';
import { useZachetCardController } from './controller/useZachetCardController';

const props = defineProps<{
userId?: number;
}>();

const { card, loading, error, reload } = useZachetCardController(props);

watch(
() => ({
loading: loading.value,
error: error.value,
card: card.value,
}),
state => {
logZachetCardComponent('state changed', state);
},
{ immediate: true, deep: true }
);

function logZachetCardComponent(message: string, payload?: unknown) {
if (!import.meta.env.DEV) {
return;
}

if (payload === undefined) {
console.log('[ZachetCard][component]', message);
return;
}

console.log('[ZachetCard][component]', message, payload);
}
</script>

<template>
<div class="zachet-card-module">
<div v-if="loading" class="zachet-card-module__state">Загрузка карты...</div>

<div v-else-if="error" class="zachet-card-module__state zachet-card-module__state_error">
<div>{{ error }}</div>
<button type="button" class="zachet-card-module__retry" @click="reload">Повторить</button>
</div>

<div v-else-if="card" class="zachet-card">
<div class="zachet-card__header">
<div class="zachet-card__title">ПРОФСОЮЗНЫЙ БИЛЕТ</div>
<div class="zachet-card__number">№ {{ card.unionCardNumber }}</div>
</div>

<div class="zachet-card__content">
<div class="zachet-card__photo">
<img
v-if="card.photoUrl"
:src="card.photoUrl"
alt="Фото пользователя"
class="zachet-card__photo-image"
/>
<div v-else class="zachet-card__photo-placeholder">Нет фото</div>
</div>

<div class="zachet-card__info">
<div class="zachet-card__name">
<div>{{ card.fullNameRu }} /</div>
<div>{{ card.fullNameEn }}</div>
</div>

<div class="zachet-card__field">
<div class="zachet-card__label">ДАТА РОЖДЕНИЯ / DATE OF BIRTH</div>
<div class="zachet-card__value">{{ card.birthDate }}</div>
</div>

<div class="zachet-card__field">
<div class="zachet-card__label">ФАКУЛЬТЕТ / DEPARTMENT</div>
<div class="zachet-card__value">
<div>{{ card.facultyRu }}</div>
<div>{{ card.facultyEn }}</div>
</div>
</div>

<div class="zachet-card__status">{{ card.statusRu }} / {{ card.statusEn }}</div>
</div>
</div>

<div class="zachet-card__footer">LOMONOSOV MOSCOW STATE UNIVERSITY ID CARD</div>
</div>

<div v-else class="zachet-card-module__state">Нет данных для отображения карты</div>
</div>
</template>

<style scoped>
.zachet-card-module {
width: 100%;
}

.zachet-card-module__state {
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
justify-content: center;
min-height: 220px;
border-radius: 20px;
background: #f5f5f5;
color: #222;
padding: 16px;
}

.zachet-card-module__state_error {
background: #fff1f0;
color: #b42318;
}

.zachet-card-module__retry {
padding: 8px 14px;
border: none;
border-radius: 10px;
background: #d80000;
color: #fff;
cursor: pointer;
}

.zachet-card {
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 22px;
background: #d80000;
color: #fff;
max-width: 900px;
width: 100%;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18);
}

.zachet-card__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 28px;
border-bottom: 4px solid #ffffff;
font-weight: 700;
gap: 12px;
}

.zachet-card__title {
font-size: 28px;
line-height: 1.1;
text-transform: uppercase;
}

.zachet-card__number {
font-size: 24px;
line-height: 1.1;
text-transform: uppercase;
white-space: nowrap;
}

.zachet-card__content {
display: grid;
grid-template-columns: 150px 1fr;
gap: 28px;
padding: 18px 28px 20px;
border-bottom: 4px solid #ffffff;
align-items: start;
}

.zachet-card__photo {
width: 150px;
height: 180px;
background: rgba(255, 255, 255, 0.14);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}

.zachet-card__photo-image {
width: 100%;
height: 100%;
object-fit: cover;
}

.zachet-card__photo-placeholder {
padding: 12px;
text-align: center;
font-size: 14px;
}

.zachet-card__info {
display: flex;
flex-direction: column;
gap: 18px;
}

.zachet-card__name {
font-size: 24px;
font-weight: 700;
line-height: 1.15;
text-transform: uppercase;
word-break: break-word;
}

.zachet-card__field {
display: flex;
flex-direction: column;
gap: 6px;
}

.zachet-card__label {
font-size: 18px;
font-weight: 700;
line-height: 1.1;
text-transform: uppercase;
}

.zachet-card__value {
font-size: 18px;
font-weight: 600;
line-height: 1.25;
word-break: break-word;
}

.zachet-card__status {
font-size: 22px;
font-weight: 700;
line-height: 1.2;
text-transform: uppercase;
word-break: break-word;
}

.zachet-card__footer {
padding: 18px 28px;
font-size: 18px;
font-weight: 700;
line-height: 1.2;
text-transform: uppercase;
}

@media (max-width: 768px) {
.zachet-card__header {
flex-direction: column;
align-items: flex-start;
}

.zachet-card__content {
grid-template-columns: 1fr;
}

.zachet-card__photo {
width: 120px;
height: 145px;
}

.zachet-card__title {
font-size: 22px;
}

.zachet-card__number {
font-size: 20px;
}

.zachet-card__name {
font-size: 20px;
}

.zachet-card__label,
.zachet-card__value,
.zachet-card__footer {
font-size: 16px;
}

.zachet-card__status {
font-size: 18px;
}
}
</style>
66 changes: 66 additions & 0 deletions src/modules/ZachetCard/controller/mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { UserdataItem, ZachetCardData } from './types';

const FALLBACK = '—';

function normalizeValue(value?: string | null): string | null {
if (typeof value !== 'string') return null;

const trimmed = value.trim();

return trimmed.length ? trimmed : null;
}

function getValue(items: UserdataItem[], category: string, param: string): string | null {
const item = items.find(entry => entry.category === category && entry.param === param);

return normalizeValue(item?.value);
}

function resolveUnionCardNumber(items: UserdataItem[]): string {
return (
getValue(items, 'Учёба', 'Номер профсоюзного билета') ??
getValue(items, 'Учетные данные', 'Номер профсоюзного билета') ??
FALLBACK
);
}

function resolvePhotoUrl(items: UserdataItem[]): string | undefined {
const value = getValue(items, 'Личная информация', 'Фото');

if (!value) return undefined;

return value;
}

export function mapUserdataToZachetCard(items: UserdataItem[]): ZachetCardData {
logZachetCardMapper('start mapping items', { items });

const mappedCard: ZachetCardData = {
unionCardNumber: resolveUnionCardNumber(items),
fullNameRu: getValue(items, 'Личная информация', 'Полное имя') ?? FALLBACK,
fullNameEn: FALLBACK,
birthDate: getValue(items, 'Личная информация', 'Дата рождения') ?? FALLBACK,
facultyRu: getValue(items, 'Учёба', 'Факультет') ?? FALLBACK,
facultyEn: FALLBACK,
statusRu: getValue(items, 'Учёба', 'Ступень обучения') ?? FALLBACK,
statusEn: FALLBACK,
photoUrl: resolvePhotoUrl(items),
};

logZachetCardMapper('mapped card result', mappedCard);

return mappedCard;
}

function logZachetCardMapper(message: string, payload?: unknown) {
if (!import.meta.env.DEV) {
return;
}

if (payload === undefined) {
console.log('[ZachetCard][mapper]', message);
return;
}

console.log('[ZachetCard][mapper]', message, payload);
}
Loading
Loading