From 81ba6cd076fe3431ca8a82fa17eb5b18bd67033a Mon Sep 17 00:00:00 2001 From: rusprus Date: Wed, 8 Apr 2026 15:29:52 +0300 Subject: [PATCH] WIP: local changes --- README.md | 107 ++++++ backend/.dockerignore | 5 + backend/Dockerfile | 14 + backend/config/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 132 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 157 bytes .../__pycache__/settings.cpython-311.pyc | Bin 0 -> 1261 bytes .../__pycache__/settings.cpython-313.pyc | Bin 0 -> 1214 bytes .../config/__pycache__/urls.cpython-311.pyc | Bin 0 -> 316 bytes .../config/__pycache__/urls.cpython-313.pyc | Bin 0 -> 304 bytes backend/config/settings.py | 45 +++ backend/config/urls.py | 5 + backend/contacts/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 134 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 159 bytes .../__pycache__/models.cpython-311.pyc | Bin 0 -> 3566 bytes .../__pycache__/models.cpython-313.pyc | Bin 0 -> 3443 bytes .../__pycache__/serializers.cpython-311.pyc | Bin 0 -> 4721 bytes .../__pycache__/serializers.cpython-313.pyc | Bin 0 -> 4360 bytes .../contacts/__pycache__/urls.cpython-311.pyc | Bin 0 -> 934 bytes .../contacts/__pycache__/urls.cpython-313.pyc | Bin 0 -> 898 bytes .../__pycache__/views.cpython-311.pyc | Bin 0 -> 15487 bytes .../__pycache__/views.cpython-313.pyc | Bin 0 -> 11148 bytes backend/contacts/migrations/0001_initial.py | 50 +++ backend/contacts/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-311.pyc | Bin 0 -> 3139 bytes .../__pycache__/0001_initial.cpython-313.pyc | Bin 0 -> 3075 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 145 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 170 bytes backend/contacts/models.py | 65 ++++ backend/contacts/serializers.py | 74 ++++ backend/contacts/urls.py | 14 + backend/contacts/views.py | 249 ++++++++++++++ backend/db.sqlite3 | Bin 0 -> 159744 bytes backend/manage.py | 8 + backend/requirements.txt | 4 + docker-compose.yml | 33 ++ frontend/.dockerignore | 2 + frontend/Dockerfile | 12 + frontend/index.html | 13 + frontend/package.json | 22 ++ frontend/public/favicon.svg | 9 + frontend/src/App.vue | 67 ++++ frontend/src/api.js | 8 + frontend/src/assets/style.css | 235 +++++++++++++ frontend/src/components/ContactForm.vue | 60 ++++ frontend/src/main.js | 10 + frontend/src/router/index.js | 35 ++ frontend/src/stores/contacts.js | 67 ++++ frontend/src/views/ContactDetailView.vue | 181 ++++++++++ frontend/src/views/ContactsView.vue | 124 +++++++ frontend/src/views/GraphView.vue | 323 ++++++++++++++++++ frontend/src/views/ImportView.vue | 137 ++++++++ frontend/vite.config.js | 21 ++ sample_contacts.csv | 9 + social-graph.code-workspace | 7 + 56 files changed, 2015 insertions(+) create mode 100644 README.md create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile create mode 100644 backend/config/__init__.py create mode 100644 backend/config/__pycache__/__init__.cpython-311.pyc create mode 100644 backend/config/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/config/__pycache__/settings.cpython-311.pyc create mode 100644 backend/config/__pycache__/settings.cpython-313.pyc create mode 100644 backend/config/__pycache__/urls.cpython-311.pyc create mode 100644 backend/config/__pycache__/urls.cpython-313.pyc create mode 100644 backend/config/settings.py create mode 100644 backend/config/urls.py create mode 100644 backend/contacts/__init__.py create mode 100644 backend/contacts/__pycache__/__init__.cpython-311.pyc create mode 100644 backend/contacts/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/contacts/__pycache__/models.cpython-311.pyc create mode 100644 backend/contacts/__pycache__/models.cpython-313.pyc create mode 100644 backend/contacts/__pycache__/serializers.cpython-311.pyc create mode 100644 backend/contacts/__pycache__/serializers.cpython-313.pyc create mode 100644 backend/contacts/__pycache__/urls.cpython-311.pyc create mode 100644 backend/contacts/__pycache__/urls.cpython-313.pyc create mode 100644 backend/contacts/__pycache__/views.cpython-311.pyc create mode 100644 backend/contacts/__pycache__/views.cpython-313.pyc create mode 100644 backend/contacts/migrations/0001_initial.py create mode 100644 backend/contacts/migrations/__init__.py create mode 100644 backend/contacts/migrations/__pycache__/0001_initial.cpython-311.pyc create mode 100644 backend/contacts/migrations/__pycache__/0001_initial.cpython-313.pyc create mode 100644 backend/contacts/migrations/__pycache__/__init__.cpython-311.pyc create mode 100644 backend/contacts/migrations/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/contacts/models.py create mode 100644 backend/contacts/serializers.py create mode 100644 backend/contacts/urls.py create mode 100644 backend/contacts/views.py create mode 100644 backend/db.sqlite3 create mode 100644 backend/manage.py create mode 100644 backend/requirements.txt create mode 100644 docker-compose.yml create mode 100644 frontend/.dockerignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api.js create mode 100644 frontend/src/assets/style.css create mode 100644 frontend/src/components/ContactForm.vue create mode 100644 frontend/src/main.js create mode 100644 frontend/src/router/index.js create mode 100644 frontend/src/stores/contacts.js create mode 100644 frontend/src/views/ContactDetailView.vue create mode 100644 frontend/src/views/ContactsView.vue create mode 100644 frontend/src/views/GraphView.vue create mode 100644 frontend/src/views/ImportView.vue create mode 100644 frontend/vite.config.js create mode 100644 sample_contacts.csv create mode 100644 social-graph.code-workspace diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef6af7f --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# Social Graph Builder + +Построитель социального графа: импорт контактов, визуализация связей, CRUD. + +## Стек + +| Слой | Технология | +|------|-----------| +| Backend API | Django 4.2 + Django REST Framework | +| База данных | SQLite | +| Frontend | Vue.js 3 + Vite | +| Граф | vis-network | +| Состояние | Pinia | +| Запуск | Docker Compose | + +## Быстрый старт + +```bash +git clone +cd social-graph +docker-compose up --build +``` + +- Frontend: http://localhost:5173 +- Backend API: http://localhost:8000/api/ + +## API endpoints + +| Метод | URL | Описание | +|-------|-----|----------| +| GET/POST | `/api/contacts/` | Список / создание контактов | +| GET/PATCH/DELETE | `/api/contacts/{id}/` | Контакт по ID | +| GET/POST | `/api/relations/` | Список / создание связей | +| DELETE | `/api/relations/{id}/` | Удалить связь | +| GET | `/api/graph/` | Граф (nodes + edges для vis.js) | +| GET | `/api/relation-types/` | Типы связей | +| POST | `/api/import/` | Импорт CSV/JSON | + +## Формат CSV для импорта + +```csv +name,email,phone,organization,position,notes +Иван Иванов,ivan@example.com,+7-900-000-0001,ООО Ромашка,Директор, +Мария Петрова,maria@example.com,+7-900-000-0002,Газпром,Аналитик, +``` + +## Формат JSON для импорта + +```json +[ + {"name": "Иван Иванов", "email": "ivan@example.com", "organization": "ООО Ромашка"}, + {"name": "Мария Петрова", "phone": "+7-900-000-0002"} +] +``` + +## Структура проекта + +``` +social-graph/ +├── backend/ +│ ├── config/ # Django settings, urls +│ ├── contacts/ # models, serializers, views, urls +│ ├── manage.py +│ ├── requirements.txt +│ └── Dockerfile +├── frontend/ +│ ├── src/ +│ │ ├── views/ # GraphView, ContactsView, ContactDetailView, ImportView +│ │ ├── components/ # ContactForm +│ │ ├── stores/ # Pinia store (contacts) +│ │ ├── router/ # Vue Router +│ │ ├── api.js # Axios instance +│ │ └── App.vue +│ ├── vite.config.js +│ └── Dockerfile +└── docker-compose.yml +``` + +## Запуск без Docker + +**Backend:** +```bash +cd backend +pip install -r requirements.txt +python manage.py migrate +python manage.py runserver +``` + +**Frontend:** +```bash +cd frontend +npm install +npm run dev +``` + +> В `vite.config.js` прокси настроен на `http://backend:8000`. +> При локальном запуске без Docker замените на `http://localhost:8000`. + +## Планируемые фичи (следующие итерации) + +- [ ] Авторизация (Django auth + JWT) +- [ ] Теги/группы контактов +- [ ] Экспорт в CSV/JSON +- [ ] Поиск по организации и должности +- [ ] История изменений контакта +- [ ] Импорт из vCard (.vcf) +- [ ] Уведомления / дни рождения diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..41f5a3a --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,5 @@ +__pycache__ +*.pyc +*.pyo +db.sqlite3 +.env diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..e90d5c3 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Миграции и запуск +CMD ["sh", "-c", "python manage.py migrate --noinput && python manage.py runserver 0.0.0.0:8000"] diff --git a/backend/config/__init__.py b/backend/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/config/__pycache__/__init__.cpython-311.pyc b/backend/config/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db475c66cbc21f1ac568e04e63fbdacd53fc0ba9 GIT binary patch literal 132 zcmZ3^%ge<81aEVs9o3~#ROK)|24wDzxz{6!s z^uMR4Fsr9=LeJo&&FEPh;nZ)8p2KOJu~|KjvpR=!I8RcT8*>QZ!ZX%k^a3vGMZowN z(|NpxOL+YmtFJ+%5-#iOctbDaO%+vcyo&WJ3{CdT=w$pgerS5OUv<2YSfhYcL*H^t zw`vFG@T7{#qv|O+t6C>OB0#DR10Rp9$niamGPf$}@y)3HgNcn(-k#QWD1M=7pPAO;kL3lz;OoEVRV-3@a!tp07urP2i zcFAKiAVMA3o$KkivQS!KSpHz(dq9kb6)FgM$Br$G0ym@?OpeWw8(kk2_l3S`oe~d+ zLioMwMC7Y+9>9fPVQE_G-feXyn(T@lX`HRild4f}CA+ckc6P$hv>gwUhXeIYn{-Em zJ_*2EfUj_TQ!L$XNKNsutrca3^mR8^V-L^9NtE;$H&!;E(q3B%C_J|=D` z95}W(iu|TST->c}(995S-F5mw{61;cFygu!2F<|HeD@Kd=^+%-i)i*hR3)R)QfOY4 z>WZWp_oW9k)sPMj@6w{!Zp%kf!?-7_no9XrSJeOlK5!DH8GA1B-o<0>hTig7Jkg7ydtTZ(Nv(2N3wFCZq;Q)HLet6Ysg9q%AnGn zIF@nPYs+H8=!oAL9jPNL53b5V%gdUHhnj3OTT;962YMYp{WtH`%;B&$zwcT|qR8>= zP#B)Q$=-pYja;%HdmJUmy_&e@!ljq%@!u9>H6lA)TB5|X3FdgEzN|VZKBi{(`0RyO7QF?Kg*-i?xZkF zqZB_a3)9l}(>oWX&tSEbo}t(zQbl%>oAHR>hS2=h)7=aH(+LMV_P%961*owC7zmz6 bH{U-;Td?>+?Huh~-ne~^%JUw42wD9HZBczI literal 0 HcmV?d00001 diff --git a/backend/config/__pycache__/settings.cpython-313.pyc b/backend/config/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0fb7b1eeb186ab2fd3356d371340ce8f43acc325 GIT binary patch literal 1214 zcmaJ=J#X7a7(P;>M3E9LE3RRra8WmCQyCGNAk9#~2tZ3zqFNRe9t|Uf0fHjW5@U)~ zk5mG4;#jmxmIB#2_Xl+8uc*+$!T|+RbjqgIe;`L&3DgX|;qJZX9=^}}#mh=X0Cau% z{jc5+IRO67F7H-;Xh80|)n3Q2yf@`n} z*JT#3QBPG^!|THv^^G@~Zk-|dz8#N}uia;MFbo@B5FuxZQ6mZ+&-NQbY)?iF7d>yB zq4S0_q9jB~jR_9jsS|r)U=n7xUPwN^b|5=p5Mys}b)q1S&nGBKZhY`)PpRb155Oq4 zW6wGDd=wE0qbRmcu{}mFLVQLXuU_Bo<{OL^-xQA}>3D!JwU*!;H|rEj zwpvu zqISFbvDUI4>(GQm>hz#VAquGdJ|x0nr`76e$Eu-`vZ3pyb!2p#daq4NE!9*r&y++z zF+0udiHJ)(;}XfxplP)YTEk=AI3Sx%-GJ7WVzyhl(V-Q9r1q{N>XE5i?T*%M{Q@qt z!6tjkC>$eY8X>G?BNR<+2Pvyltt?xqT(?|_P#k-~Fp?+dgx{mZoBHS>zD1p7e{}RS z-4;29Vg3YLe~D`^`5zx%h&wZJ=MVALg}6HtcQ3?yGx1)^zpgw^g*jhNx%v9WOD?Va zn=jVbR9b8R?#?`~EV!J+rouu1)$O#rC;(2H*W`J1>($<@dix(PS1l}ZT!l?b3kgVD o)R(mRYI`Pqk&2YO^Ms*pDeKCm0B(NrJJ@{v(N{B2!#gy_xBG@|&j0`b literal 0 HcmV?d00001 diff --git a/backend/config/__pycache__/urls.cpython-311.pyc b/backend/config/__pycache__/urls.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2396867166f60d3d62428cfaea528c5fc809fe3b GIT binary patch literal 316 zcmZ3^%ge<81n+aVW~Kt^#~=<2ut6E0oq&w#3@HpLj5!Rsj8TlaOi@gX3``8Ej44b& zk|~9G86yM3Y9NMyWlTUG5JNyZV-#}=OE7~b>r0S;CetmJg2a-HTkM&6$vLGdsZ}hA z1)2I)yvg}_C5g!;#d@VhImLdOEVsB*vJ&&s^TGTgW}wz079eqp8z>Jnpd_^@uXrWH zXONk{g!K~(3iN>{q-Cb-gY@eaRQ}?y$<0qG%}KQ@0{Ic-x?*P_@qw9IH1* t0~-S?XM;v)V#=|b%jOi12YppQv){$7I6U00RWnsP3Zst literal 0 HcmV?d00001 diff --git a/backend/config/__pycache__/urls.cpython-313.pyc b/backend/config/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c109547bd21b302058345093b685f45332ba6bf6 GIT binary patch literal 304 zcmey&%ge<81n+aVW~Kt^#~=<2ut6E0O@NH448aV+jNS}hj75xIOhrrz3^9yK48cq> zOhA&^lo6uVlnFxXGZrzYvuLuu1aUQ)Zm|?3mSo&w&&*5CDNRYOVo5B>)UV=A&d)1J zOfD(bD=o??_S0m!#hsFsn3tXp<`*#owHC1eiCf%2d7uF$sYQ9kD;Yk6%)DixpOK%N zs$W{1TBKi|UzA;3keHmRU!0$unV6%SUX)mnp`VnPoSmANq7SqtEi+voWQkru&DFIE5&AD9^#8SgWweq>`{Bxf&%^I z{JfIHYLjjei7w$;&V9nF2ed5h1JXAdF_MAI*VmoPj zGjn|B-rqfQ?m6e4-{hM}BqTul_vCW^MoCS*LCnDKf9B3*CJgK3&~QJM$C9<3Xf@pDX|sZjH2{>4BCCfK1T z2&k^bP{+o)I#3=!g*Tx>TcG4ks4z##EmlP~Sry@!?hVXh_dIyu5!`OeL^mO#9f%$t zgdPY&tYgnHEq+(X#I*#tzt;(z(Tim@TQ+|Is@a!pm91E}*%EtZ-A=K`DeF_VYJJA4 zF!J1*VJk5IBE_Cq6}DvEWy=FEe}ZMEAJ=}tURd|i9w#)ZeykKoF;|{&)=OG_q~T z`g}C)cY+FckD@psMS){r76|lZMY&16d#(mbQ;vk@(HLOR65|~ZmG!?`@6P+jrh~QZ?@i0KzMrw+F888u z0gUbfk!`vJ`U3XxpF4*3qg^{taA)BzDb^+~B=I##aVs%yC&numYtjDcn|8E+;mDH4&i|f8uUgTocJylHTvOZJ zAUit3r9l=uX2p)#v166@n}Z8w78|r;gLZ7N@_t?N`hH#) zHj7s{_IhI?B>LW}3!8=OD|A!Z4;7&+s}~AH&6zDn=Q&mbJnb;=AE$Xz)J!ZHY}Tq+ zci0m?tWM?&Q*LdTg97jrk3M@0>sEM;cm^O@&B9U2L!B#TiF*tmz$6qoEN9l|>^lGj zubD=^NDRZ=!(YK`(JE+KW#6UP_fTpopm@dllT`t5!;#{ zqTgCVI_Zn}fy_3D$|W<%n@5;Cyk$Cws$&IGn>63P!X+)v_NYP&kgyiy$tvf096h`kLpQLz<)+vdX^6G$-pbaI4q8f^!o3t6&FL?Y{tv@!R?{&KLq5_{5 zqzuQOFN0HzRkcw`+cZIa%#pI2Wb>E-)(j1Une;FS6H6zMRF!>?yvXfl)wINU$_ZpA z^n4a7M?fRvs#z#={S=d6%+0y4Ad9N7&?T>BipfJGAlkA9D(c)VNqh*$4hJ?p+~kkLs*9RLAj6yG*` zh(*uAH8=G43vO>|&^|Kkc2Ne;HK;1y;Z&Le9WG6UMs!N@x#9&f#hC%Vy-r;|b82XK z!&dq>pq=gyhA*BPJO7(eW$eS>4qv9Ha4@V9BTMro)NNhzwM{pL!Ubu}P zL9~QOI?1GyKScVbuh@~kS}y62Sfw-qGG)1Mm Z-rFbQF1D7_H)lS1#phoC7X;t!e*o6g=VSl? literal 0 HcmV?d00001 diff --git a/backend/contacts/__pycache__/models.cpython-313.pyc b/backend/contacts/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..57083dcad2c6796413e2cb19abe986597cb50fcd GIT binary patch literal 3443 zcmai1-EZ606({vAnUbup#IX}cOPsYzFllYvBEhn)X>2cv6R$Jf0Ry}ev_#8XXi~W( zo4V+OnhcqP25>X9NH7eA+lS5Blaa)$n#At@giGzg8i4`D_T;yAO0m6d=a7=WYITHl zbb0SNN9Ue<&hK~Kj7B32JpY+pk*|gr=AZb|e*CRLQ-Q&U4A1cFD07h|Y?kR^jxfBZ zm*KsW?6|LOu=V|t*BujYf#H2WV~NkP@PkF*|5*6T!CYtq&2y|BnAJ*>q8GP3GfbfM z(i~zjtVxEjJVQJ@OT4^?_$K}1+;(+$4B>itAMZcyAwkgS-cuvM2l)_aggg2O^8r4> zcb)cpESgkFD!j`XFn)OV_NMv<5Rz ztlM-I)}LnSL#s-ctzXlX{KkLL7Eg!Xqff1coX3vLif;*uq?U~tn|o6tQ<^RbsyHi6 z*#6;JQC2|5Kc$H36%q!!4g6s+~_L-Dho1G!0EqwQGV{DnYr0 z+V49G4$91Vnn&Pkcfl2)ws|k{^F9*b{Ulfp0xFAf+L^l*!l~fHTLK-PaL{`gA zqU_Z6tP2&zB%)$x1VJ}MLoN!UVGw!BG$dURNCqNl!iTZX6lSzpsbK07DO}UY6}=)B zrGl;%Wl{NFnTVB{!jxFNBB`Z9(JdUk;C5=hGH3g6*96Z2_ok;o++-T*-uF+ZX`e)E0{5sRK2(`TJhd=)YmSC`+(>X~ESDizTVy|q9ux;apudtZG)rgcgzk z@zkQ^HsD=Y1f)Yuv)-ehfl_G7)S;<#-NZTtL_t7W15a!8vn>4_@TLlmSFJl%6}0`D zF(VNZE2|w1D6Z_lksvJM)9i*y-HjuiiEFY0VYLX&*O{(5S|ZW;Px~Aa!*bvC09l46 zIX>)AosUF;VD{GH_$VLa<3NXNkYlpWk!{kH1MjuGogytyx*+O;u9>8L&i9;hShIf9 zDh96|)>HxR7?J_q>X<>(A-J~ibB8B@wX$TmHTMTB2ZZ5foJo3|&H7AyIUUWVoY<+ECfd!pb*` z=o?fNZ70>`q91@2wuze)JJ8w@UIMprZEk<1H3wyYkoH8zxT(rlO-V2`$PBcAyRAO( zw^5)Fc2L%0;r)kKOb-GKZ?X~yfZQ2&B6j+6DEG~ z+AvSf<78J!(u+i{ASsbNIxV1ZP6?ZE&oexQ3n)-S|2+s~nODAurq-kV^=N-16mKMZ z?&k008=2#c%yVCb{jorG3;SCJOAdwJGb8Xn!&LX%KrcgMWz`5 literal 0 HcmV?d00001 diff --git a/backend/contacts/__pycache__/serializers.cpython-311.pyc b/backend/contacts/__pycache__/serializers.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbec5767c70a34f390b89f38a07e3aa647b3f892 GIT binary patch literal 4721 zcmb7HO>7&-6`uX$zkWw8m%+6!TtP+)|T!9q1EXNU*SA_y! z6tY55%!(Z0$rMq=`$UyAZr-g&o(P0Ig};-sK3mHNTK@0Q^4nSg&l@yvTDo?t z?R+0;LXDunuP|%y+lWY>1nN^q(Y3(t4v%_iW-x&qLME#sXSL& z&{(8IXO)Hgl09)2D3|m+E&-DkN)=6Kp&Zqeil)j+g@qQ&s+;el^|jLFTxoHkk`iec z21%m`F@!ikCgo=VS;lTY^J^XZ8BJZS*-%p7Im#K!-Ue?^E!4rJWPk+#bO|p z8?D(*EFJHpP8YQUP+va{uuQ&*^jVQ(TanaeBxOcUT9K30j*@MG>p4{ugkZ>C&rB*n zUx3OqfiQr8i^*PS7yxYFTuFT$Ibc)B-U50YRSzN9+k&&8xW6_advr9Mrj1U#{OG2Y zc-c&xw-V=umbA{<0$qY;`oJq3Ww>hKqX&+!)-@E6ng{11PD z#6XJg{1yQH#X7t@lgd2-@A1_0AkWp~TlH?*TSVg(?4zC<9!3>fweN9vI6NjY6!(Fq z0T!r zD1K&qX?$jU=~VnPsQfJr-bw)H;TU({TX5jQ!2z2$Nok3uczO&vWg?U*Eq*C*%hA-9 z4t3#?D@0$QUjbMq)o|a#bH6+H=&BhWvcf~_@s9>S9Naiz9-Xj`PHcuJjPS(Q(IG23 zR_)Ji3w*41mjKiV-_u+3k^cBfxbBvM?Zm5ABodt0_kxpP40`g6N z2cXFxMPGV!(u%&cara-M5uG-p(^hnP`C7GmF?y?v?X9{YdaDeF-pZ3kB`IDbo#1#Y z98(VdUh?cZ3q=iYrE*yTyph-xn1Q@0tN}mt9a6u_+r9_`oM1h`>6@ZTd9LA(7Du2( zw#JD9pZvm{53ZR?jJ)>E7 z=jDp*ck2OsmscSyyTe~;3)#Rd_)N}g3Tpb{kAck2?*UfHEV)B|dFd`#P~*ghyjP?7 zWPcxGVED!X+v$Dsfv_&RU(N5}^V4D3o$aLB9wYR~=(5y$G;OY9!DP=cfm~X5)MiXER(Ne2Oqz> z6&u-%jhL|$R_w$|pvHUS=_QFHwLbXV#*yvT=U)$gS_qA7unGMe$ozkVMkcg|0qY*q zpfNACatnwLhS`<)&F4Xj31FCGo4~t}shhx+{h0t9VEY&p&>a`TPUYDC>ZoK*0yCHd zGvMmRVEO@Dxud3p(6 zjHAt_L9akrcbjlrRu`N25meDN0JMo9+Jp!FXcIk-O$;X2!qvpUl@_c1^@=(MEjab zUx&gLyMQ>*x%BE?48xvQcf+tnxX}8K0l+ZghpqUFTk(m__{6iTpUs)^DJwp;6~DF_ zzh=g#t@!jxu*Q3%?I(%v)egYtHjaC@zAD}|A=GvrGrugWrJO9=Aq5@5slg2u&I`EC z;&DN*LJ_@z&=%jF$1(&w!u6j6EPKDIc-jz8e-jy7xn@Pi%*ePE8DE~Pwtd&c5Z(hk z7!2cQCpf1qc9nq(cQ4iSiabLh?*4U&-gIIipMA&GZD(cg66>4BTW{lpbgngcWV29H zsXdF7&-6`uVgxl8KD6itz~Wzmi;o3~ zJ4>dpfC6d|7%E`EHVVVhp>>NMs+{Wn1WA*$Xix2tG!Ikg4ZpH0bx{0l$aA~h=YpF!mzQHjcplj|JiVnpRt zVVu7%P~o~r#Tbc@!$cML5mlPxQnp9eiSs@yIB)8)Ej?iA{Q^s`ZRrC`87zBS{oz;B zr#z#?{cG41=a}CzXjac<7Yu5xaoEN)@2h6MsHcm}mojpCF>B`2XzdoUQK`QUm4{@K zP);S3S2-$7ik<%EFge@k()b@9W+|NIokr@#m7qRVqVkLi4it>MzP%%Rbj z)Q@+V1CRnXH$!ZOotXA)@#8dhQlZbk~XnTX3s{ooCk(n8;N z$ZBu@lC;T-L1oFasgMIVxT-HgW1yplL8RSXggQaQ<5SOpct~z?DR{S+6k4RG|Mc}G=RX~wKg66=8REyH_Zfd>XwB+5{uD8 z@M3+S)waDkE{^WTT?ey0(BFCy#3K1D&{GS%Pzk(H3&bmdcr|cxbyv%Vz;&Hk7X(>x z))W^#8CPfo-ZYA0ABz1baN*eA?T~`)nl&@8AwX;p+8aXSXgY`jeK}t7rzmc!Ef8@Q zHo@+O57olwo`%m=!xt96^>5KD-QYF|%4b1QYRm~crr~K8bbzH!L3}y(K=xF{3q19F z9>1tk!@^-fn=hOE;!}YYsjSLWLFQvYm({jcV-1&V!S8JQ2u9ntVS_yl7|6|tTJSr& zZ=1CImJ8*tN=W^?C*v^E0hm$T=kP@W>cG@CC~cNt9%mk`F(0R~%SD}5yj?JuKVw*F znk^tx*_6{lKWD%veF=pF8@VHFJZOMZp#vxmqUeMV3}<>81ll&EKhhHY$>&5rMlM3M zz5v3GemslXiprgBv_hL=w-${&_LaXS76O#UzeTq8i}I|14A1{}R`T)+-prPQQxo*siinFwuq3#<6vax4wD zzXM`i!C2R{cHq&j0xQ{cBbKz|TX!iu@w7a=dzIwCW%IG&oQT4nGYJulAg3v!H1u7L}*sq?8JPqOI&mO}t-d z;Zp^hNHASdAYJj>=}ONS9*{e@1ZP$n0#W`+Wf7qAP7Dz9F~H}otTl4Wx){sDd2Pi` z#*EC2VU4t$f=HKk99#r^KO}E9r<-abx!FWgE|+dMH8ZU|f0OScZwMBjAQsfF%@L9p zZVD-k=(Yl_IBr{k)()9zH)OS->9CaDWK>A?v^jxK_k4btJKj)cpm&UXl~H%}g>`W9 z=@IdDeqx>wKYUw!UN{2MsZC=XGfrXNj4`d3a&F$mq?|qlnUYk@7ISdA&rq{e0FK3F zWL`MOjdamsf}YDcwA&5s6whQDL<`A++HCPTh1|lv~+yeRy5Ud-rOTeR(Ng5e?9W`C3ksFBs+*M>J4bY$( z-I-6?34oT#T7{f`o1TYWEwF`z#<|y7jH8`!Yco@4+}f&3;L*AR0&p8TPzxQYgpSlg zqm|I;lWQN&RzqX8(0C;@UJXqw$s0iu9<1+?0mcC6L4a|W3*$)iv47dW8t#8I_h1e{ z3IMG{&s3u?Az%lV2Wru=l|!`?S1Tv3R!@v=`8|MK2!+87^tMi98@W)mk^8&LgID&7 zjcg{+>F8o!P0N^RxcVT)(vu+Y1_xve*FEH7n#A_+qUe<5Tt)>7WOxg^-ywe&U;Hf4 zUkeOB4GdQUiN&j{Enl0WA}P-zem5P)-jJfRZfaOXZJI)+i5D&>8}Qf{Im_lF8wspu z0Y+c|u?jL06pXBJ4rUHBIm@ogJ!XidYB| zLk1)|GNnSD`9BiLL%0cvsavFOop_GpilEZxyVKqK-uJ!l&S$w?1~Br@kLH;G@H;vr zq0Bay6-2&*38r9yEmQ?ttcpT(7Oj*`ss#5blUR~1S7iZ2pk#p5^F#-#3RS+PuaAFi zOo_@ZHAxIaWZ{fMfd8^&WKDfXmH)D^zhEl3id1Q3XJRnGJDlT;W1cf*q)~I>7FpuaBM;aRg~I1RVh zW|q%VD9Z>ge2Gy%s2wuHrpGROS7g(^K9iz~=xJH>_qiPBd79Bi(?@4K|0?#fkIwdJ zz_s0(qa;D1PCPU9Rx^#j;Q7Q~ZR(F}vAx>$t+GhL2SItq{|=Yf7_RQ?hUe)?i5`g- zy_2i-3)?l@7JbQmgYmlQcpP`_IG0jS<-l$>uaOR3F9Kq!f};T(sQZ3GWp~WGf_F6IIg0ZZ`VZ1H9cI Dc+Tis literal 0 HcmV?d00001 diff --git a/backend/contacts/__pycache__/urls.cpython-313.pyc b/backend/contacts/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07ad4019aaf28f11e0ffddacf42962cecef72c1e GIT binary patch literal 898 zcmZ`%&2G~`5MDcL=a0nDM4_!xQBpV9X!B`i=`bB>U zlVq6Xi??BX36o-&;{P!k#7ZZvUOVFuRiw}}{4R}Kj%f#NqzW|i1nnAu?Kj-ONBE3O zWmV~wh4u+en6Bd+rcY=RBiryT*C7=2uRT?-({%f# z;dI<`fNg?fj21)YXaOVQw|247L;Eh?E92>0f+ddN9|QA0K;^Jb6BuG)IDvg<1N^Xf3Kj^MrXq@;+4 JX?&B(`3rUM(&zvH literal 0 HcmV?d00001 diff --git a/backend/contacts/__pycache__/views.cpython-311.pyc b/backend/contacts/__pycache__/views.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5efed7a113a7bb34af132d5686b275e59a26f796 GIT binary patch literal 15487 zcmcIrdr(`~nZH*L^hR$aKr)w?4K@hu7!qTehXG@Q?HJ=YF|DJ@NMP9lk$Z(-kv8Pf zDM@Ifc-^(?+C}lWHQl5$#I0-kNZRgfyP2Kc*?Tp22ldV{r ziCkLX(tQq>T}A{{e*HN4eQ3qI@Go6{!)wI%A^2`Cf*7eN!CAhJ)8RM1MvOT9CZLsK zD)YMhE$dQM`N?6T+4=`8@kp~wP@fM5E---zBa|@_Uu2rWINwyzi}6C4Z!{7NPd1Z+ z`dEOO3QsbDKVa>f<$|gwJQ;z{_$A~+E}{Hb-=U|DAAI_V_xQ=9eZ#{-{-;-YfGcYj6fP6YbtQah>KRfsAvY|F*sO1dx zyrF(pme!l*4)OZNWqor>-@MSYxR296%IhDUCDS^6h9EVbwAp&C@=E2x&UhtfZspCb zv-{FU^Q`7$$dzc;32Lub5{97ldMCpE=@1A;(0f4$zL4~V)&Q%Z3ZFh3fa(i!UnnG~ zb26g!_|e3ys<_mAv2B7OD~!=VDCG4vlk^r~W>8I@q-%iErW`Tghan))(%fI#STXm~ zvav2@tmBLgys=^CK-$s7Dr^~rQfbK$n#jn* zA5jHh%=zW0i-Iv{1hNATbTd3nj|POYh>wQ;KhmtFYav6r4oN){oFeE3AVN848}x&h zM!OU6(~bCLD-x6;y$#3@2&vJch__=bl1lhzx`5>5J42+64&GR^Z1kjzo`s7`PR_WG zH||?D?oS!_bH)R_@c@dv8ALAKQnr|xAn)%xUSKHD1c`>?KOq2S0%-0!yJ{Gcs{=Xr z4WEUgilKobqGWGzyeyI@=)630U74RHOo)DYq3PIE*>b-E%9gJyTM^N%WgnGA6@KLn zS)r+ct}UD13ta3K_CxkQdzYJG7rEEjMfM)|3dR0Ne0{Bpnhg5`47Gy__{ZVN{+Rta z_ZD?N$au~&o&t&D$g;x!$G3r8BgTm+aq2J{w6pLoT@YTCt)JMqg%MSb8x1O?Sd|xq zS0@e#uS$2kNOFbk%+Fy8476W-Pz2}#PelhZq`ClAt%Me#{ zjITMy*@t=iFtR?++S`+tG8&~u3q}cuydY<3(CmOh=@uXbU5u7h?Bef2Alt7P$f-DlIJD^m$6Nq3C z%@Hhvypc;&fgGnsvTI(@0*z$aFj$FGTmiq=A7rLNzDt4*hQ=rzoWee*dm!Ux8T}>9 zpQQ7s4aGzBWpIZL?n;Ok1p1cR*Jz+uKw2cFw+wx9`H9 zJ;mC4lD_4wPo=g##cl23xAvsnTW)wW1{HQS68UPLn7i23prlH1j0;L}h=VY&n?>s_ zS!*r6wj#ly*{q};@DS9w1p<8#Uo|-iG4ueuGuS1tn#d}>pj}gR`Y?PFXVz~)V21c( z8DVl1&zu?As#2B{ma2>qULWER_yoyJ-ybc6xso?g%ci!Jsclic)Wn&3cvH`^sXt}v z=S&BA)4`d88QE%kErg{yV};j;*z!-1KrR)8-j)a~1X$Z{xH(-1uj^nH9im_gG#`zS zI9)srfx=ZKn*YeEo)bi|F)lPmpqa91oYvTP*k7_s+{<8me}-oDL-v>4tK7@nE9?^c z3+_$sYm^xLC49I`0oz^fEwIw}*q;_!U6ivpBvGQ-A*uZeR3}El;**TRX6uFwhJ;QW z642M@eIZycibKHzNt)$ZjXf`rAqJb6>oZAf>vH`njud7iD}@@*Gys_)SldDAe!pR{ zc5x{2!op70-imkbLssWRRibf{f@K!!leo;1ETm9gSSCFT-%(@?t};Z0(c7d9Kk&el zvmNAt>IQh-0IL{S-+8FEV&~lkxxluR#5p@A8z*A&C^1EjLzl(LQ>`&YRFMSM^%pGb zVcyEy3d+xk9Zp9}rce~7kV-i<8dFA<$@K+5j9*!tB1pu_B3SS26)j8Vf(1{1S#e5# z(bje2nYPgM##B)iL(U!Z!}B~rYy4{Qsf@~|$fznNoAX9VF+|S!=Mp8`T`Vch7nGDg zrYvZ?vP9cAkXi{&?O#Mn>ZqEwN97w&W>Ixv86+_mh?8?|;yBbL(TJ?OqD5Q4F@gAMoz4E+z_??d)S?1vOC7%28<@b&>%iADBjNU6i&$>6AuYT~{D z5if&z`H-7|wVZEsG(0^S@nE863d{>E_8`xDkX~BsLE?KD!M%nVwou~YrG)~%MfMlC z90blK4~=F49}qI0EaY z(_#N5dKcsZa|kZ)1vwL;VJjPfDHPC)sj0xEpWcJtHG)1@0%jv9r_O{Y19Ueg(qU!{ zZf;?Br#ua-rgMTEDgaDSIzt}?Ci)Z*2In!bX3`}tzJvyI0CH0)Mm3OwX&PNt4apj7$I?*?O z@cO~z80Tu`U9GGO;yLGT-no0(*_Cp3an5eu*&SCx{;*LtyVEvT+FFrljCaSolRXO^ z$wSFQX;*FHNb;F@U%c5Va$U6p?9YZO{ z5a$@?9m8=&+Umf{wx(@0%eJj4+ty@+v$gQH7S`7C>3@R+AzM7J#AF4z%JP>LMRg_VZ;11kOq{!373Z$JxdP;syx)2y3?#Q6)yVD^IfYvYuv!WzOZt;mjU44)OxUy;p}rTL4L zrQTFo>t~!V(k_i(w=rZBG3F!-s|S5l71c!5pT8HLYS?sM>hU5dvruj^ZB)z1U~bbz z5cSJmu(sK_hl^GPg?W7i(!_Kzeary!YF;1aawUkUHmZ;6{AyT}82pACMVdy!>TT|6 z`2Si|4kI4sPk(uoI8Vgov{72)`O9&-kuDf7@>3+NF7iq$S?|AwW|orCUr00may{%< z$8;qnV=U~$?E3bJSb2e5%S-5ksVH?tv=pbgh;yB7fI8}E8A(i%b5BBz^(AX;Db5LP z+NmdE#)29fOVrp}l)55Xit|s`SRd8r)z}2JHkYimt*Bgaw{q$mF-t+MEhTDgFG^hz zEyY<;#JOJ1rcIUTD9UF?$r7DKoTcklS;V=%Zsw>dE75O8P0ZgF)U?!it18O1qGTWZ zN!e^_ekzn-sah4wuXL@7<+n~Mo2r%h^KEX_6g6*3n$<)NbQdL>3{qXnF!V2g%{xt4LD%{9~i1B)*$ z?m*a^xE+K2)FN!PU=PNy(S5ox6|=OOaP@OKZQe*(#X0ZZ)?zqV5Bd_k62 zIY6LZM#iq}wu$=ki*<;4*L6`Ik0;iG^i(!^cE8zk>~Qg(vl)`VSeM;+LedxOa2p2! z$PG7;5UR`W!0KA4I%)fepE38?saR~I5)d1M_JNFkD&?QefsyhZ3(`zPJRO90$OrO; zlg<2?Z0&QIv)^Uk=ibIGGVX*aI>5jw0F!O_ivWCy@2j=S$*eFHwrf>ZdF>P%A|?@3 zSvk-rVFf}z14K||t4=?QQO{wNcCG%>PFu5DdqFo5^!wqY6FcHFk~#ty?PHj*8_8)T z2#?WtnuB02Y>(+d3>(C4vv+cO;&gxprvWJ-l^&RcLt$~J&q(`f4IW9rVFo0|%y9>f z@0Y|<1^L|bUIkXpblD+skE-f&aF zsocEE4ZCBdEp4>U>Q+?dwdjY$0I95fV5zv)bfqb=hqE;Dmd07tiq0M%zZv0l4ZN=5 zAyKWN($#hId#~?Jp69APe6?rx(2B7#(egtDXKdw-fK*f(sI;RJHsbE)3}H7{KSUDm z%We8AI7bujXj(X!a&)kcj_<^W%FAfZ^PZAdzP5L{b|6(dz||h&YY!#L(lvGS7q4IZVc9}tF|c&vSI60o z0j_C~ZyMxk4)HaIGK7q(Vvi0lAAK%$^tlyRU2^wA4!YUrPy`G;pd>iqSoe>}x`hI!90=iHG{CZ>~tw9_@OyRJ(& zG^HE2w{TpFw3z@@S3A)%_Q{salUR)P?bI6#t_t%S{q*a&oCO?>cy&YrNb zIya|tr>*wcUO3H5Z0GHb8KT}`eTXDZB1ZJoeBbrHWDDnN<6Uj6t7CB&=j`O2oy*Sd zl(U<2KF&KI2Q&#Vo1OC=*E^EcTy-m7-O5%Yn&jBcJ9aNSx>AlV&e6>~xq@!sj`6mUr3Myv-S52Hxzx-((#t>63lV@pxdwUH zV2Ltd9LUqv?zlSba*JZz!MQxV%fq^MFV2A}~#>Etcb>TY1X~|i^xr>o_sr5| zu5*Cz9DsoyRmZui6MWSPsa`wpblmD# zu)ouNyPK^aSloHP<8H@NRaRp4D6yJBzGiT_W++ty2gSpD%`jBRWV@!jqD#Bb8sOcq zEloA8xT&Q1j{TNB8RKd@_}UJ(Hn3#=rTu62r5Ly8Fu&(8M6g50mWNKHhE8BxhrIld z7bBsq?lImyw(Jh2+#$|A$-5_EjJN?$1pihx7#_2O53Ic)22HI< zW_!{`OWZ&E;_QnzFE2DKFz+?rJI>W~@ikquFQy&UiJq&MSQ91PpQd$&nS&ybC@79T zJzVJgQ4R(ffr^jY)d(koj~Bd1R}$81>#z0jUzB9rk=1i&0ARqeAV?Ote5?YE?ty0ALbc)*ErMwZ7xOVy#GT^=fTy@5_2l)EZ7c%O=ra-Y9Lip2 z(MLec|hR55Ll zp6a98?+`a-o6U7pc9QTLV%om}?@Q@=iYX7=z`7y)gBYlAOih)H@|Pi-D*)zd4D1j_a;S+gg-=bM;*ZIQfe} zq@;5#&$UqD$lR1<)`3J{q@>RWd<$j2nVQGSqvcFJzyNvHVXc*ZdsOeQxC{~BA^eV; zMTZn50WND5m%*0A6aeg2M9}>zd!4P$g_5pZDE}EP-;~VO!zX{O-dPV{{k2LehP22p zq6eFjUNPMB*DI+QF8V?x`Q0~^?`qbNrVfAyfK-6yV>ME;HM|CryqE)Ez8HKWD8Qi{ z{38Q29j2#oDrav4xb#!@=M;D;+(WbqL8p5FJ}t5zc)-gCmqWA+$Yq6RlsSJ{GaVUg zeXNxUj$c-SGiiA8vQmthj$>vw5eR!#Of8~b+#Chj0X&7`2EgnC2)}}nd0?3&UJ7s> z#JaFxz`6j?dW*_Owu=2Gvi*eJQ{*_YCNIzQQ&_1D;56==*&+Z%`Vf3$=THl>T`oa) zP(x8J+?$^1Cd}o#+$>b@eGzoRHbHCN7ZJZjY>mjvE_P8zBp60<1axG&3=`f4b^9(o z2|@ZC5_HI*(II>q7hUW-P?H&`ga~HgqE9TYi-NpS3a^Uv6>ubzkVjFF;PpXC_})ff zQWnc8{YfU(QJr<6Wx$7483{+g8ACNn2hibLP%-C%;MoCABBH7=&2B-t|KOp%VF}^E zWvr+gg6+WZ<43)_+uObSp6c};>l=EeZ}_;BzcLiQ0KaxngMSEfAs9IW_)fqFIr;;m z;GHbUL7fC`FQ7)pAQ~8D!LXnn2Dg&Q@qQtq}z73Y48cR$8i_VAWHvpp-;y5u9AbsKNp2Kk`#iNau6anz*k;50+j zydVQ=r)4YEwS0AFMk#l~<`c*#NDybRWaNJ)2we#PPD9lLB?a%Nz~Ql031yX|UQhHxnD50SiHHcQSvvSPHP^~SgQ-{_C; zfBosXr?IuB1T%l(`i12AZ@+ZoB{5>L{{FVR+rU3|R}a6dM~uQ$2i`dFtrLmf6_YjI zbFKeMf8r2lYvygutPOw{&eYDE+Luk8DN`qBdW<(cw#GNe*_wD;6KnGRJE~i%1#KQA%DK9DSJ$%Z$&~9!&b5zs?b~d*8C_Wgs236?q1pP7 z&}cwxKr$k+5gJo`=j-}e)$6L?>#S*Wb=q2mx5Wt_{dVvg2l9|m>Q$d0!CxJzm`1P) zzpmm6bwtE$CB2q_4< zwO?m3;_tPjp9&#pBj^uflI#zWq~Bw0$ApKG3?mswf(BkRh_nYo6cPjpK2vu~KN@?S z{xQ5F*2CNe3L}ps(}Z?L{7Vy>8SyVo=w`&f2gD9`qdQG>vK!rLqJ=HI9}pdE={rq4 z&6d71Wf~If(|Qtrj<;_I6(PZdt$uaN$n5}_tt0U>e8rB3_yMr)+*d6j)6O_&J7%|W zq=ko4WKOa(LjZXwCuG*un9g;4GI9`%9?~^VF-~hI+u^VwN7|nwNl@rKdR``F);Zh6 Zkx+y+BkLnc&{d?EnXM>J`Xct?{{bsdEp-3@ literal 0 HcmV?d00001 diff --git a/backend/contacts/__pycache__/views.cpython-313.pyc b/backend/contacts/__pycache__/views.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc822aa8d4edaeb6a695b7716e04c6ff807e8935 GIT binary patch literal 11148 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 UT(kHr<|g_c(}t%9w6Zb$7f&g{xBvhE literal 0 HcmV?d00001 diff --git a/backend/contacts/migrations/__pycache__/0001_initial.cpython-313.pyc b/backend/contacts/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7840bc68df90e357cc6bc3d5c48da6410e46be92 GIT binary patch literal 3075 zcmcguO>7&-72aKvE0U%pQa_eNSr#QLb|_nXLQ_jVsOH^({n@y3SwdyxL-P8z>RAUv7@;e8Hqh@a&y@Pxl05Fz8v zh1n1ZcXNXr3Z*#|Uf}0EZw&|j_-x1@Nu(%bBRlRSE3kM;EfU#M4c%m7$tYmWyrlR* z|CtIVk1m7#``iLYc*GF_@g#%<63%lZvLMd2>^1RaHyVf~I@h-Ent%ipK_Y6J3_l~F zDBwEw#l=t@9hwvXlK{-&eK7)RMlGmyG6bmPfu01ELTv%6{RyZT>IhJsPedIKP+frP zJ|M1c)Z@jT22A<@Oz$KQaUXdCqR&O-W9TUAN6|cwGPw{Mm<%1r``}OJ{n$Y#%yVey zWga~RoE=9eygPdOAS~Qu3}pj0e*C1v;OEGGTr9NCWqzoiOYWl^+($R`LmfJaPN5N$ z^Wx9{ANL)FeaHNL^CCKp#?dolJbKo@x4b~w(HYWFoX9@MViy|)xYGm&m}gB>hHlA< z#RRo5!-N;7vtgEyZOf1pf@KRA*r75}OEOuNmhmc!nV6`urryLXv4Y8>VPZ*_OPGb- z=gzm_(-z@D*Xe!dwpa1;4N1d#(OM#I*|G>O$*Q*VZ%=-~m9rrhS=3~GnYh&-pvp^z zj(1Lj!gkH*M|6YUqZ`g|=@xy!5(X*Cx_Z-VCAM3p^h0Njeo8C!0o|lu(2DaG-E?lV zXxT7Tx8K-qU!Z>g>wENb0Bt#MIJca4SVT80Z2EhA0Ot2$k6ZLE-OLK46HNSY;sF_w zY-Ra?AKkbv$%O)o+vS2wl)o1BxPvv{iZqe_koVT+fe~b3LbV_B- zCbGt&1}OmHdJ#~qV9dm8D%J`nYck4y7mdzg&0~QzlR%QJ)iTbCKz>XyG!4r|8$XWb zo=c<46s(Kah>G=s9rd_z)|@x#r=D6asoJW~>m4}3XD(Uv6R>;W@p~6Q30ZmFmQ_d| zUBUjzK7d6yC>Y##-lksyD7t8ys*X+5?(??rE^!MyZP70?^eecgHE{gEdCyq`bi}Zh zFtHPRq5RQ!goVuujI|5hM=j z2bs0ny860}CCh+afW$v@m$IEKDtTc@5{pX`yl!m`gPf2gxH-)Y5N55CbWJ6urNKSv z1}K_T9V!NrFHyi{$PTP4DmIDxK4*ufyryu`eRGqrt4W+wi)W!-%(!{UVpB^p@x{1T z+ktu>->`hKc^YbEUM=AOHDeI07WG-^En?x=&3m{_)65AR_ZIg}OWWG{-7w!FuD!Gy zrcYLew#Vs}1tu?DT0gS^AD2Iz8{Cuh8Vxda7@IrPe=Q z?H}LnaQbIx^86mv=DgE?jwYve+v6QAyWQNtSpCFgz3AN z_lc*@+&NwwJY5|;{dbOgem1>P{wCAAY(d13PUi1CZpdVo7FxUa@|Bd}dx|NqoFsLFF$Fo80`A(wtPgB37U#kg>)5K;i>4 PBO~Jn1{hJq3={(ZGAAEc literal 0 HcmV?d00001 diff --git a/backend/contacts/migrations/__pycache__/__init__.cpython-313.pyc b/backend/contacts/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2942d470188846187cf70dd5c50ff9b47db0152 GIT binary patch literal 170 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 + + + + + Social Graph Builder + + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..7c1f718 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "social-graph-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.4.0", + "vue-router": "^4.3.0", + "pinia": "^2.1.7", + "axios": "^1.6.7", + "vis-network": "^9.1.9", + "vis-data": "^7.1.9" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.3", + "vite": "^5.1.0" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..9d242db --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..61c9f2e --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,67 @@ + + + diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..d41a0a8 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,8 @@ +import axios from 'axios' + +const api = axios.create({ + baseURL: '/api', + headers: { 'Content-Type': 'application/json' }, +}) + +export default api diff --git a/frontend/src/assets/style.css b/frontend/src/assets/style.css new file mode 100644 index 0000000..d30941d --- /dev/null +++ b/frontend/src/assets/style.css @@ -0,0 +1,235 @@ +/* ===== Reset & base ===== */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --bg: #0f1117; + --surface: #1a1d27; + --surface-alt: #22263a; + --border: #2e3354; + --accent: #5b8dee; + --accent-hover: #7aa5f5; + --accent-dim: rgba(91, 141, 238, 0.15); + --text: #e2e6f3; + --text-muted: #7b82a6; + --text-dim: #4a5075; + --red: #e05c6b; + --green: #4ecca3; + --orange: #f4a261; + --radius: 10px; + --radius-sm: 6px; + --shadow: 0 4px 20px rgba(0,0,0,0.4); + --font: 'Inter', system-ui, -apple-system, sans-serif; +} + +html, body { height: 100%; background: var(--bg); color: var(--text); font-family: var(--font); font-size: 14px; } +#app { display: flex; flex-direction: column; height: 100%; } + +a { color: var(--accent); text-decoration: none; } +a:hover { color: var(--accent-hover); } + +/* ===== Layout ===== */ +.layout { display: flex; height: 100vh; overflow: hidden; } + +.sidebar { + width: 220px; + flex-shrink: 0; + background: var(--surface); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + padding: 20px 0; +} +.sidebar-logo { + padding: 0 20px 20px; + border-bottom: 1px solid var(--border); + margin-bottom: 12px; +} +.sidebar-logo h1 { font-size: 15px; font-weight: 700; color: var(--text); } +.sidebar-logo span { font-size: 11px; color: var(--text-muted); display: block; margin-top: 2px; } + +.sidebar nav { flex: 1; } +.nav-link { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 20px; + color: var(--text-muted); + font-size: 13px; + font-weight: 500; + transition: all 0.15s; + cursor: pointer; + border-left: 2px solid transparent; +} +.nav-link:hover { color: var(--text); background: var(--surface-alt); } +.nav-link.active { color: var(--accent); border-left-color: var(--accent); background: var(--accent-dim); } +.nav-link svg { width: 16px; height: 16px; flex-shrink: 0; } + +.sidebar-stats { + padding: 16px 20px; + border-top: 1px solid var(--border); + font-size: 11px; + color: var(--text-muted); +} +.sidebar-stats .stat { margin-bottom: 4px; } +.sidebar-stats strong { color: var(--accent); } + +.main { flex: 1; overflow: auto; display: flex; flex-direction: column; } +.page-header { + padding: 20px 28px 0; + display: flex; + align-items: center; + justify-content: space-between; +} +.page-header h2 { font-size: 18px; font-weight: 600; } +.page-content { padding: 20px 28px; flex: 1; } + +/* ===== Buttons ===== */ +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 14px; + border-radius: var(--radius-sm); + font-size: 13px; + font-weight: 500; + border: none; + cursor: pointer; + transition: all 0.15s; + line-height: 1.4; +} +.btn-primary { background: var(--accent); color: #fff; } +.btn-primary:hover { background: var(--accent-hover); } +.btn-secondary { background: var(--surface-alt); color: var(--text); border: 1px solid var(--border); } +.btn-secondary:hover { border-color: var(--accent); color: var(--accent); } +.btn-danger { background: transparent; color: var(--red); border: 1px solid transparent; } +.btn-danger:hover { background: rgba(224,92,107,0.12); border-color: var(--red); } +.btn-sm { padding: 4px 10px; font-size: 12px; } +.btn:disabled { opacity: 0.4; cursor: not-allowed; } + +/* ===== Forms ===== */ +.form-group { margin-bottom: 14px; } +.form-group label { display: block; font-size: 12px; color: var(--text-muted); margin-bottom: 5px; } +.form-control { + width: 100%; + background: var(--surface-alt); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + padding: 8px 12px; + font-size: 13px; + outline: none; + transition: border-color 0.15s; +} +.form-control:focus { border-color: var(--accent); } +.form-control::placeholder { color: var(--text-dim); } +textarea.form-control { resize: vertical; min-height: 80px; } + +/* ===== Cards / Table ===== */ +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px; +} +.table { width: 100%; border-collapse: collapse; } +.table th, .table td { padding: 10px 14px; text-align: left; border-bottom: 1px solid var(--border); font-size: 13px; } +.table th { color: var(--text-muted); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; } +.table tr:hover td { background: var(--surface-alt); cursor: pointer; } +.table tr:last-child td { border-bottom: none; } + +/* ===== Badge ===== */ +.badge { + display: inline-block; + padding: 2px 8px; + border-radius: 20px; + font-size: 11px; + font-weight: 500; +} +.badge-colleague { background: rgba(79,172,254,0.15); color: #4facfe; } +.badge-friend { background: rgba(78,204,163,0.15); color: #4ecca3; } +.badge-family { background: rgba(244,162,97,0.15); color: #f4a261; } +.badge-business { background: rgba(91,141,238,0.15); color: #5b8dee; } +.badge-acquaintance { background: rgba(123,130,166,0.15); color: #7b82a6; } +.badge-other { background: rgba(255,255,255,0.06); color: #9ba3c5; } + +/* ===== Modal ===== */ +.modal-overlay { + position: fixed; inset: 0; background: rgba(0,0,0,0.6); + display: flex; align-items: center; justify-content: center; + z-index: 1000; +} +.modal { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 28px; + width: 460px; + max-width: 95vw; + box-shadow: var(--shadow); +} +.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } +.modal-header h3 { font-size: 16px; font-weight: 600; } +.modal-footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 22px; } + +/* ===== Search ===== */ +.search-bar { + position: relative; + margin-bottom: 16px; +} +.search-bar input { padding-left: 34px; } +.search-bar .search-icon { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + color: var(--text-dim); + pointer-events: none; +} + +/* ===== Alert ===== */ +.alert { padding: 10px 14px; border-radius: var(--radius-sm); font-size: 13px; margin-bottom: 14px; } +.alert-error { background: rgba(224,92,107,0.12); border: 1px solid rgba(224,92,107,0.3); color: var(--red); } +.alert-success { background: rgba(78,204,163,0.12); border: 1px solid rgba(78,204,163,0.3); color: var(--green); } +.alert-info { background: var(--accent-dim); border: 1px solid rgba(91,141,238,0.3); color: var(--accent-hover); } + +/* ===== Empty state ===== */ +.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); } +.empty-state svg { width: 40px; height: 40px; opacity: 0.3; margin: 0 auto 12px; display: block; } +.empty-state p { font-size: 13px; } + +/* ===== Loading ===== */ +.spinner { + width: 22px; height: 22px; + border: 2px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.7s linear infinite; + margin: 60px auto; +} +@keyframes spin { to { transform: rotate(360deg); } } + +/* ===== Graph container ===== */ +#graph-container { + width: 100%; + height: calc(100vh - 80px); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} + +/* ===== Scrollbar ===== */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } + +/* ===== Utils ===== */ +.flex { display: flex; } +.gap-2 { gap: 8px; } +.gap-3 { gap: 12px; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.text-muted { color: var(--text-muted); font-size: 12px; } +.mt-1 { margin-top: 4px; } +.mt-2 { margin-top: 8px; } +.mt-3 { margin-top: 14px; } diff --git a/frontend/src/components/ContactForm.vue b/frontend/src/components/ContactForm.vue new file mode 100644 index 0000000..76bd457 --- /dev/null +++ b/frontend/src/components/ContactForm.vue @@ -0,0 +1,60 @@ + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..296cb09 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import router from './router' +import './assets/style.css' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..27cef8c --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,35 @@ +import { createRouter, createWebHistory } from 'vue-router' + +const routes = [ + { + path: '/', + redirect: '/graph', + }, + { + path: '/graph', + name: 'Graph', + component: () => import('../views/GraphView.vue'), + }, + { + path: '/contacts', + name: 'Contacts', + component: () => import('../views/ContactsView.vue'), + }, + { + path: '/contacts/:id', + name: 'ContactDetail', + component: () => import('../views/ContactDetailView.vue'), + }, + { + path: '/import', + name: 'Import', + component: () => import('../views/ImportView.vue'), + }, +] + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +export default router diff --git a/frontend/src/stores/contacts.js b/frontend/src/stores/contacts.js new file mode 100644 index 0000000..dd8de93 --- /dev/null +++ b/frontend/src/stores/contacts.js @@ -0,0 +1,67 @@ +import { defineStore } from 'pinia' +import api from '../api' + +export const useContactsStore = defineStore('contacts', { + state: () => ({ + contacts: [], + relations: [], + loading: false, + error: null, + }), + + getters: { + contactById: (state) => (id) => state.contacts.find((c) => c.id === id), + totalContacts: (state) => state.contacts.length, + totalRelations: (state) => state.relations.length, + }, + + actions: { + async fetchContacts(search = '') { + this.loading = true + this.error = null + try { + const params = search ? { search } : {} + const { data } = await api.get('/contacts/', { params }) + this.contacts = data.results ?? data + } catch (e) { + this.error = e.message + } finally { + this.loading = false + } + }, + + async fetchRelations() { + const { data } = await api.get('/relations/') + this.relations = data.results ?? data + }, + + async createContact(payload) { + const { data } = await api.post('/contacts/', payload) + this.contacts.push(data) + return data + }, + + async updateContact(id, payload) { + const { data } = await api.patch(`/contacts/${id}/`, payload) + const idx = this.contacts.findIndex((c) => c.id === id) + if (idx !== -1) this.contacts[idx] = data + return data + }, + + async deleteContact(id) { + await api.delete(`/contacts/${id}/`) + this.contacts = this.contacts.filter((c) => c.id !== id) + }, + + async createRelation(payload) { + const { data } = await api.post('/relations/', payload) + this.relations.push(data) + return data + }, + + async deleteRelation(id) { + await api.delete(`/relations/${id}/`) + this.relations = this.relations.filter((r) => r.id !== id) + }, + }, +}) diff --git a/frontend/src/views/ContactDetailView.vue b/frontend/src/views/ContactDetailView.vue new file mode 100644 index 0000000..1f22597 --- /dev/null +++ b/frontend/src/views/ContactDetailView.vue @@ -0,0 +1,181 @@ + + + diff --git a/frontend/src/views/ContactsView.vue b/frontend/src/views/ContactsView.vue new file mode 100644 index 0000000..73ea561 --- /dev/null +++ b/frontend/src/views/ContactsView.vue @@ -0,0 +1,124 @@ + + + diff --git a/frontend/src/views/GraphView.vue b/frontend/src/views/GraphView.vue new file mode 100644 index 0000000..e963cf6 --- /dev/null +++ b/frontend/src/views/GraphView.vue @@ -0,0 +1,323 @@ + + + + + diff --git a/frontend/src/views/ImportView.vue b/frontend/src/views/ImportView.vue new file mode 100644 index 0000000..4186e8c --- /dev/null +++ b/frontend/src/views/ImportView.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..3b15456 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + optimizeDeps: { + esbuildOptions: { + sourcemap: false, + }, + }, + server: { + host: '0.0.0.0', + port: 5173, + proxy: { + '/api': { + target: 'http://backend:8000', + changeOrigin: true, + }, + }, + }, +}) diff --git a/sample_contacts.csv b/sample_contacts.csv new file mode 100644 index 0000000..4946bf38 --- /dev/null +++ b/sample_contacts.csv @@ -0,0 +1,9 @@ +name,email,phone,organization,position,notes +Иван Иванов,ivan@example.com,+7-900-000-0001,ООО Ромашка,Генеральный директор,Познакомились на конференции 2023 +Мария Петрова,maria@company.ru,+7-900-000-0002,Газпром,Аналитик данных,Коллега по проекту X +Алексей Смирнов,alex.smirnov@mail.ru,+7-921-000-0003,Яндекс,Разработчик, +Ольга Кузнецова,olga.k@tech.ru,+7-931-000-0004,Сбербанк,Менеджер проектов,Знакомы через Марию +Дмитрий Волков,d.volkov@startup.io,+7-950-000-0005,ИП Волков,Предприниматель,Потенциальный партнёр +Анна Соколова,anna.s@design.ru,,Freelance,UI/UX Designer,Дизайнер сайта +Сергей Новиков,s.novikov@gov.ru,+7-812-000-0006,Администрация СПб,Советник, +Екатерина Морозова,katya@fintech.ru,+7-962-000-0007,ФинТех,Финансовый директор,Знакомы с 2019 diff --git a/social-graph.code-workspace b/social-graph.code-workspace new file mode 100644 index 0000000..362d7c2 --- /dev/null +++ b/social-graph.code-workspace @@ -0,0 +1,7 @@ +{ + "folders": [ + { + "path": "." + } + ] +} \ No newline at end of file