ساخت اپلیکیشن مدیریت هزینه با جاوا اسکریپت — از صفر تا صد

۳۸۸ بازدید
آخرین به‌روزرسانی: ۷ شهریور ۱۴۰۲
زمان مطالعه: ۱۱ دقیقه
دانلود PDF مقاله
ساخت اپلیکیشن مدیریت هزینه با جاوا اسکریپت — از صفر تا صدساخت اپلیکیشن مدیریت هزینه با جاوا اسکریپت — از صفر تا صد

ساخت اپلیکیشن مدیریت هزینه با جاوا اسکریپت یکی از بهترین مثال‌های کاربردی برای آشنایی با این زبان برنامه‌نویسی محسوب می‌شود. ما هرگز یک زبان برنامه‌نویسی را به خوبی یاد نخواهیم گرفت، مگر این که شخصاً چیزی را با آن بسازیم. بنابراین با ما همراه باشید تا با بررسی یک مثال عملی با این زبان بیشتر آشنا شویم. در این مقاله فرض شده است که خواننده محترم دانشی ابتدایی از HTML ‌،CSS ،Bootstrap 4 و جاوا اسکریپت دارد.

997696

مقدمه‌ای بر ساخت اپلیکیشن مدیریت هزینه با جاوا اسکریپت

ما در این راهنما قصد داریم یک اپلیکیشن جاوا اسکریپت طراحی کنیم که با استفاده از آن بتوانیم میزان مصارف هزینه‌ای یک فرد را در طی بازه‌های مشخصی اندازه‌گیری کرده و بفهمیم که چه مقدار از درآمد فرد صرف پس‌انداز، چه مقدار صرف سرمایه‌گذاری و چه مقدار هزینه شده است.

طراحی رابط کاربری اپلیکیشن مدیریت هزینه

نخستین مرحله‌ی کار، طراحی یک UI برای اپلیکیشن مدیریت هزینه است. شما می‌توانید از ایده‌های خود استفاده کنید و یا سری به وب‌سایت‌های Dribbble یا Behance بزنید تا ایده‌ای از طراحی‌های مختلف به دست آورید. برای ایجاد UI می‌توانید از نرم‌افزار Figma استفاده کنید. دلیل این که کار را از طراحی UI آغاز می‌کنیم این است که کدنویسی ساده‌تر است. انتخاب یک طراحی موجب می‌شود که تفکر شما نظم و سازمان‌دهی پیدا کند و در نتیجه سرعت توسعه اپلیکیشن افزایش می‌یابد.

ساخت اپلیکیشن مدیریت هزینه با جاوا اسکریپت

کدهای HTML موردنیاز

در ادامه کار خود را با ایجاد یک فایل index.html در پوشه پروژه آغاز می‌کنیم. ابتدا باید بوت‌استرپ را به پروژه خود اضافه کنید. به این منظور می‌توانید از راهنمایی‌های مستندات رسمی آن (+) بهره بگیرید. همچنین ما از فونت Open Sans برای طراحی ظاهر بهتر استفاده می‌کنیم. این گزینه کاملاً اختیاری است.

اکنون که اسکریپت‌ها آماده است، اقدام به تحلیل طراحی خود می‌کنیم. برای شروع باید دو کانتینر داشته باشیم که کانتینر آبی در سمت چپ حدود 40% فضای صفحه را اشغال و کانتینر سمت راست بقیه صفحه را پر می‌کند. پیاده‌سازی این حالت به کمک Bootstrap Grid کار آسانی است. در ادامه کار خود را با کانتینر آبی سمت راست آغاز می‌کنیم. کد HTML ما به شکل زیر است:

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <title>Expense Manager</title>
6    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
7          integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
8    <link rel="stylesheet" href="style.css">
9    <link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet">
10    <link href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.css" rel="stylesheet">
11    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
12            integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
13            crossorigin="anonymous"></script>
14    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
15            integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
16            crossorigin="anonymous"></script>
17    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
18            integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
19            crossorigin="anonymous"></script>
20    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.js" crossorigin="anonymous"></script>
21
22</head>
23<body>
24<div class="row">
25    <div class="col-4 left-container">
26        <div class="month-container">
27            <div class="header fs-white">Your Budget</div>
28            <div id="current-month" class="sub-text fs-white"></div>
29            <div class="budget-container p-2 mt-4">
30                <span id="month-budget" class="month-amount">0</span>
31            </div>
32        </div>
33
34        <div class="chart-container">
35            <canvas id="expense-chart"></canvas>
36        </div>
37    </div>
38    <div class="col-8 right-container">
39        <div class="calc-container">
40            <div class="header fs-dark-grey">Track Your Budget</div>
41            <div class="dropdown open">
42                <button class="btn btn-info dropdown-toggle"
43                        type="button" id="dropdownMenu3" data-toggle="dropdown"
44                        aria-haspopup="true" aria-expanded="false">
45                    Expense Type
46                </button>
47                <div class="dropdown-menu">
48                    <a class="dropdown-item" id="type-savings">Savings</a>
49                    <a class="dropdown-item" id="type-expense">Expense</a>
50                    <a class="dropdown-item" id="type-investment">Investment</a>
51                </div>
52            </div>
53            <div class="mt-3 tracking-text text-capitalize sub-text bottom-border">Tracking Savings ?</div>
54
55            <div class="row mt-4">
56                <div class="col-7">
57                    <input class="form-control input-expense-description" type="text" placeholder="Description">
58                </div>
59                <div class="col-4">
60                    <input class="form-control input-expense-value" type="number" placeholder="Value">
61                </div>
62                <div class="col-1">
63                    <button type="button" class="btn btn-success btn-submit-expense"></button>
64                </div>
65            </div>
66            <div class="expense-list mt-4">
67
68            </div>
69
70        </div>
71    </div>
72</div>
73
74<script src="app.js"></script>
75</body>
76</html>

کد درون تگ body کارهای زیر را انجام می‌دهد:

  1. یک کانتینر به نام left-container ایجاد می‌کند که شامل month-container است.
  2. این کانتینر دارای title و current month و همچنین budget واقعی برای ماه مربوطه است.

همچنین از پول محلی استفاده خواهیم کرد.

سپس به سراغ بخش افزودن پس‌انداز یا مصارف می‌رسیم. این کانتینر شامل یک عنوان، یک منوی بازشدنی برای انتخاب نوع هزینه و چند فیلد ورودی برای ثبت هزینه‌ها و همچنین یک لیست برای نمایش مداخل بر اساس تاریخ است. این کد زیر left-container قرار دارد.

بدین ترتیب فایل HTML ما آماده است. اینک یکی از سه فایل مورد نیاز اپلیکیشن خود را تکمیل کرده‌ایم. در ادامه باید این فایل HTML را استایل‌بندی کنیم. آیا متوجه فایل style.css که در تگ <head> ایمپورت کرده‌ایم شدید؟ در ادامه صفحه وب خود را با یک استایل ساده و زیبا استایل‌بندی می‌کنیم.

کدهای CSS مورد نیاز

از آنجا که در این پروژه از بوت‌استرپ استفاده کرده‌ایم، اغلب بخش‌های CSS به صورت خودکار برای ما ایجاد شده‌اند. بوت‌استرپ بخش زیادی از موقعیت‌یابی و طراحی را انجام می‌دهد و از این رو کار زیادی برای انجام نمانده و صرفاً باید حاشیه‌ها، اندازه‌های فونت و رنگ و ظاهر صفحه وب را تغییر دهیم.

فایل CSS ما به شکل زیر است:

1* {
2    margin: 0;
3    padding: 0;
4    box-sizing: border-box;
5}
6
7.clearfix::after {
8    content: "";
9    display: table;
10    clear: both;
11}
12
13body {
14    color: #555;
15    font-family: Open Sans;
16    font-size: 16px;
17    position: relative;
18    height: 100vh;
19    font-weight: 400;
20}
21
22.left-container {
23    height: 100vh;
24    background-image: linear-gradient(#0277BD, #03A9F4);
25    background-size: cover;
26    background-position: center;
27    position: relative;
28}
29
30.right-container {
31    height: 100vh;
32    width: 100%;
33    position: relative;
34}
35
36.header {
37    font-weight: 700;
38    font-size: 36px;
39}
40
41.sub-text {
42    font-size: 22px;
43    font-weight: 400;
44}
45
46.month-container {
47    padding-top: 25%;
48    padding-left: 5%;
49    padding-right: 5%;
50}
51
52.calc-container {
53    padding-top: 12%;
54    padding-left: 5%;
55    padding-right: 5%;
56}
57
58.fs-white {
59    color: #ffffff;
60}
61
62.fs-dark-grey {
63    color: #4e4e4e;
64}
65
66.budget-container {
67    display: inline-block;
68    background: #ffffff;
69    border-radius: 8px;
70    box-shadow: 0 6px 4px #000000;
71}
72
73.month-amount {
74    font-size: 36px;
75    font-weight: 700;
76}
77
78.bottom-border {
79    border-bottom: 1px solid #00446D;
80}
81
82.expense-row {
83    padding: 10px;
84}
85
86.expense-date {
87    color: #077CC1;
88}
89
90.expense-text {
91    color: #077CC1;
92}
93
94.expense-list {
95    overflow-y: scroll;
96}
97
98.fs-15 {
99    font-size: 15px;
100}
101
102.expense-value {
103    text-align: end;
104}
105
106.expense-saving {
107    color: #039300;
108}
109
110.expense-cost {
111    color: #E40000;
112}
113
114.expense-investment {
115    color: #f48803;
116}
117
118#expense-chart {
119    margin: 20% 0;
120}
121
122.btn-submit-expense {
123    border-radius: 50%;
124}
125
126.currency-select {
127    margin: 0 4%;
128}
129
130.selected-currency {
131    color: #ffffff;
132    font-size: 12px;
133    font-weight: 700;
134    margin-top: 1%;
135}

یکی از مشخصه‌های جالب CSS، قابلیت ایجاد پس‌زمینه‌های گرادیانی است. این قابلیت به طور خاص در مواردی به کار می‌آید که بخواهیم کمی رنگ به پروژه خود اضافه کنیم.

اگر در فایل فوق CSS مربوط به left-container را بررسی کنید، می‌بینید که مشخصه پس‌زمینه تابع liner-gradient را می‌گیرد که یک جهت و رنگ‌های مورد استفاده در گرادیان را می‌پذیرد.

1background: linear-gradient(direction, colour-1, colour-2,);

جهت گرادیان دارای مقدار پیش‌فرض top-to-bottom است.

ساخت اپلیکیشن مدیریت هزینه با جاوا اسکریپت

با این حال برخی اوقات یک گرادیان دقیقاً مطابق آن چه در ذهن شما است عمل نمی‌کند. در این موارد می‌توانید از مشخصه radial-gradient در CSS بهره بگیرید.

به این ترتیب کار ما در بخش‌های HTML و CSS پروژه پایان یافته است. در ادامه روی فایل app.js کار می‌کنیم و تعامل‌ها و کارکردهای پروژه را به آن اضافه می‌کنیم.

توسعه اپلیکیشن مدیریت هزینه با جاوا اسکریپت

جاوا اسکریپت مهم‌ترین بخش این پروژه است. بخش‌های HTML و CSS شیوه نمایش ظاهر پروژه مدیریت هزینه را تعیین کردند، اما اینک باید روی منطق اجرایی آن کار کنیم. فایل app.js جایی است که همه اتفاقات مهم رخ می‌دهند.

پیش از آغاز، زمانی را صرف تأمل در این خصوص بکنید که چه کارکردهایی را باید به فایل app.js اضافه کنیم. در حال حاضر زمانی که یک نوع هزینه را انتخاب می‌کنیم یا یک توضیح و مقدار هزینه را اضافه کرده و روی دکمه‌ها کلیک می‌کنیم، هیچ اتفاقی نمی‌افتد. همان طور که حدس می‌زنید باید eventListeners را اضافه کرده و ماه جاری را نمایش دهیم.

ابتدا باید تابع‌های مختلفی بنویسیم. برخی از این تابع‌ها صرفاً مسئول مدیریت UI و منطق هستند و بودجه ماهانه را محاسبه می‌کنند. این تابع‌ها را کنترلر می‌نامیم. پروژه ما 3 کنترلر خواهد داشت:

  1. کنترلر اصلی: این کنترلر تعامل‌های اولیه و کلی اپلیکیشن مدیریت هزینه را کنترل می‌کند.
  2. کنترلر UI: عناصر UI از قبیل تغییر دادن رنگ فونت و ایجاد لیست مداخل و غیره را کنترل می‌کند.
  3. کنترلر هزینه: بخش محاسبه را کنترل می‌کند و مقادیر کاربر را گرفته و بودجه ماه جاری را محاسبه می‌کند.

به این ترتیب باید یک فایل به نام app.js بسازیم. تگ اسکریپت مربوطه را به انتهای فایل index.html و درست پس از تگ پایانی body اضافه کرده‌ایم.

1<script src="app.js"></script>

فایل app.js ما به صورت زیر خواهد بود:

1let ExpenseController = (() => {
2    let total = 0, savings = 0, expenses = 0, investments = 0;
3
4    return {
5        inputEntry(userInput) {
6            if (userInput['expenseType'] === 'savings') {
7                savings += userInput['value'];
8                total += userInput['value'];
9            }
10            if (userInput['expenseType'] === 'investment') {
11                investments += userInput['value'];
12                total -= userInput['value'];
13            }
14            if (userInput['expenseType'] === 'expense') {
15                expenses += userInput['value'];
16                total -= userInput['value'];
17            }
18        },
19
20        getSavingsData() {
21            return savings;
22        },
23
24        getExpensesData() {
25            return expenses;
26        },
27
28        getInvestmentData() {
29            return investments;
30        },
31
32        getTotalData() {
33            return total;
34        }
35    }
36
37})();
38
39let UIController = (() => {
40    let expenseType = 'savings';
41
42    let HTMLStrings = {
43        inExpenseDescription: '.input-expense-description',
44        inExpenseValue: '.input-expense-value',
45        btnSubmitExpense: '.btn-submit-expense',
46        expenseList: '.expense-list',
47        currentMonth: '#current-month',
48        typeExpense: '#type-expense',
49        typeSavings: '#type-savings',
50        typeInvestment: '#type-investment',
51        trackingText: '.tracking-text',
52        expenseChart: '#expense-chart',
53        monthBudget: '#month-budget'
54    };
55
56    return {
57        numberFormat(number) {
58            return Intl.NumberFormat('en-IN').format(number);
59        },
60        showCurrentMonth() {
61            let now, month, year, months;
62
63            now = new Date();
64            month = now.getMonth();
65            year = now.getFullYear();
66            months = [
67                'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October',
68                'November', 'December'
69            ];
70            document.querySelector(HTMLStrings.currentMonth).textContent = months[month] + " " + year;
71        },
72
73        getHTMLStrings() {
74            return HTMLStrings;
75        },
76
77        setExpenseType(type) {
78            console.log('here', type);
79            this.expenseType = type;
80            let emoji ="?";
81            if (type === 'savings') {
82                emoji ="?";
83                if (document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-warning')) {
84                    document.querySelector(HTMLStrings.btnSubmitExpense).classList.remove('btn-warning');
85                }
86                if (document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-danger')) {
87                    document.querySelector(HTMLStrings.btnSubmitExpense).classList.remove('btn-danger');
88                }
89                if (!document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-success')) {
90                    document.querySelector(HTMLStrings.btnSubmitExpense).classList.add('btn-success');
91                }
92            }
93
94            if (type === 'expense') {
95                emoji = "?";
96                if (document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-warning')) {
97                    document.querySelector(HTMLStrings.btnSubmitExpense).classList.remove('btn-warning');
98                }
99                if (document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-success')) {
100                    document.querySelector(HTMLStrings.btnSubmitExpense).classList.remove('btn-success');
101                }
102                if (!document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-danger')) {
103                    document.querySelector(HTMLStrings.btnSubmitExpense).classList.add('btn-danger');
104                }
105            }
106            if (type === 'investment') {
107                emoji = "?";
108                if (document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-danger')) {
109                    document.querySelector(HTMLStrings.btnSubmitExpense).classList.remove('btn-danger');
110                }
111                if (document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-success')) {
112                    document.querySelector(HTMLStrings.btnSubmitExpense).classList.remove('btn-success');
113                }
114                if (!document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-warning')) {
115                    document.querySelector(HTMLStrings.btnSubmitExpense).classList.add('btn-warning');
116                }
117            }
118
119            document.querySelector(HTMLStrings.trackingText).textContent = "Tracking " + type + " " + emoji;
120
121        },
122
123        getUserExpenseInput() {
124            return {
125                description: document.querySelector(HTMLStrings.inExpenseDescription).value,
126                value: parseInt(document.querySelector(HTMLStrings.inExpenseValue).value),
127                date: new Date().toLocaleDateString(),
128                expenseType: this.expenseType ? this.expenseType : 'savings'
129            }
130        },
131
132        addListItem (inputObj) {
133            let html, element;
134            element = HTMLStrings.expenseList;
135
136            if (inputObj['expenseType'] === 'savings') {
137                html = '<div class="bottom-border"> <div class="row expense-row"><div class="col-2 expense-date fs-15">' + inputObj['date'] + ' </div><div class="col-8 expense-text fs-15"> ' + inputObj['description'] + ' </div><div class="col-2 expense-value expense-saving fs-15"> ₹ ' + this.numberFormat(inputObj['value']) + ' </div></div>'
138            } else if (inputObj['expenseType'] === 'expense') {
139                html = '<div class="bottom-border"> <div class="row expense-row"><div class="col-2 expense-date fs-15">' + inputObj['date'] + ' </div><div class="col-8 expense-text fs-15"> ' + inputObj['description'] + ' </div><div class="col-2 expense-value expense-cost fs-15"> ₹ ' + this.numberFormat(inputObj['value']) + ' </div></div>'
140            } else if (inputObj['expenseType'] === 'investment') {
141                html = '<div class="bottom-border"> <div class="row expense-row"><div class="col-2 expense-date fs-15">' + inputObj['date'] + ' </div><div class="col-8 expense-text fs-15"> ' + inputObj['description'] + ' </div><div class="col-2 expense-value expense-investment fs-15"> ₹ ' + this.numberFormat(inputObj['value']) + ' </div></div>'
142            }
143
144            // Add the new element
145            document.querySelector(element).insertAdjacentHTML('beforeend', html);
146
147            // Clear the input fields after adding element
148            document.querySelector(HTMLStrings.inExpenseValue).value = "";
149            document.querySelector(HTMLStrings.inExpenseDescription).value = "";
150        },
151
152        updateOverallTotal(totalValue) {
153            document.querySelector(HTMLStrings.monthBudget).textContent  = "₹ " + this.numberFormat(totalValue);
154
155            if (totalValue > 0) {
156                if (document.querySelector(HTMLStrings.monthBudget).classList.contains('expense-cost')) {
157                    document.querySelector(HTMLStrings.monthBudget).classList.remove('expense-cost');
158                }
159                document.querySelector(HTMLStrings.monthBudget).classList.add('expense-saving');
160            } else {
161                if (document.querySelector(HTMLStrings.monthBudget).classList.contains('expense-saving')) {
162                    document.querySelector(HTMLStrings.monthBudget).classList.remove('expense-saving');
163                }
164                document.querySelector(HTMLStrings.monthBudget).classList.add('expense-cost');
165            }
166        },
167
168        displayChart(savings = 0, expenses = 0, investments = 0) {
169            let ctx = document.querySelector(HTMLStrings.expenseChart);
170            let expenseChart = new Chart(ctx, {
171                type: 'doughnut',
172                data: {
173                    labels: ['Savings', 'Expenses', 'Investments'],
174                    datasets: [{
175                        data: [savings, expenses, investments],
176                        backgroundColor: [
177                            'rgba(32, 137, 56, 1)',
178                            'rgba(255, 84, 98, 1)',
179                            'rgba(255, 206, 86, 1)'
180                        ],
181                        borderWidth: 0.5
182                    }]
183                },
184                options: {
185                    legend: {
186                        labels: {
187                            fontColor: 'white'
188                        }
189                    }
190                }
191            });
192        }
193    }
194})();
195
196((UIController, ExpenseController) => {
197
198    let HTMLStrings = UIController.getHTMLStrings();
199    let setupEventListeners = () => {
200        document.querySelector(HTMLStrings.btnSubmitExpense).addEventListener('click', addExpense);
201        document.querySelector(HTMLStrings.typeExpense).addEventListener('click', () => {
202            setExpenseType('expense')
203        });
204        document.querySelector(HTMLStrings.typeInvestment).addEventListener('click', () => {
205            setExpenseType('investment')
206        });
207        document.querySelector(HTMLStrings.typeSavings).addEventListener('click', () => {
208            setExpenseType('savings')
209        });
210    };
211
212    let setExpenseType = (type) => {
213        UIController.setExpenseType(type);
214    }
215
216    let addExpense = () => {
217        let input = UIController.getUserExpenseInput();
218        console.log(input);
219
220        if (input.description !== "" && !isNaN(input.value) && input.value > 0) {
221            console.log('Adding item');
222            UIController.addListItem(input);
223            ExpenseController.inputEntry(input);
224            UIController.updateOverallTotal(ExpenseController.getTotalData());
225            UIController.displayChart(ExpenseController.getSavingsData(), ExpenseController.getExpensesData(),
226                ExpenseController.getInvestmentData());
227        }
228    }
229
230    let init = () => {
231        console.log('Initializing...');
232        setupEventListeners();
233        UIController.showCurrentMonth();
234    }
235
236    init();
237
238})(UIController, ExpenseController);

در ادامه بخش‌های مختلف این فایل را توضیح می‌دهیم.

عبارت‌های تابع با اجرای بی‌درنگ (IIFE)

IIFE-ها تابع‌هایی در جاوا اسکریپت هستند که به محض تعریف شدن، اجرا می‌شوند. این تابع‌ها در زمان اجرای فایل اسکریپت بی‌درنگ فراخوانی می‌شوند. از این رو نیازی به تابع دیگر برای فراخوانی شدن ندارند. تا پیش از ES6 شکل آن‌ها چنین بود:

1(function(){
2    console.log('Welcome, this is an IIFE!');
3})();

در ES6 شکل آن‌ها چنین است:

1(() => {
2    console.log('Welcome, this is an IIFE!')
3})();

در ادامه منطق تجاری اپلیکیشن خود را بررسی می‌کنیم. نخست کار را با بررسی کنترلرها آغاز می‌کنیم.

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
gitconnected
۱ دیدگاه برای «ساخت اپلیکیشن مدیریت هزینه با جاوا اسکریپت — از صفر تا صد»

برای من پرش داره اصلا نمیاره

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *