├── .browserslistrc
├── .editorconfig
├── .env.dist
├── .gitignore
├── LICENSE
├── README.md
├── angular.json
├── contracts
└── Gallery.sol
├── hardhat.config.js
├── karma.conf.js
├── package-lock.json
├── package.json
├── scripts
└── deploy.js
├── src
├── app
│ ├── app-routing.module.ts
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── components
│ │ ├── home
│ │ │ ├── home.component.html
│ │ │ ├── home.component.scss
│ │ │ ├── home.component.spec.ts
│ │ │ └── home.component.ts
│ │ ├── images-by-author
│ │ │ ├── images-by-author.component.html
│ │ │ ├── images-by-author.component.scss
│ │ │ ├── images-by-author.component.spec.ts
│ │ │ └── images-by-author.component.ts
│ │ └── upload-image
│ │ │ ├── upload-image.component.html
│ │ │ ├── upload-image.component.scss
│ │ │ ├── upload-image.component.spec.ts
│ │ │ └── upload-image.component.ts
│ └── services
│ │ ├── gallery.service.spec.ts
│ │ ├── gallery.service.ts
│ │ ├── ipfs.service.spec.ts
│ │ └── ipfs.service.ts
├── assets
│ └── .gitkeep
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.scss
└── test.ts
├── test
└── sample-test.js
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json
/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # For the full list of supported browsers by the Angular framework, please see:
6 | # https://angular.io/guide/browser-support
7 |
8 | # You can see what browsers were selected by your queries by running:
9 | # npx browserslist
10 |
11 | last 1 Chrome version
12 | last 1 Firefox version
13 | last 2 Edge major versions
14 | last 2 Safari major versions
15 | last 2 iOS major versions
16 | Firefox ESR
17 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.env.dist:
--------------------------------------------------------------------------------
1 | # Add your private key from Metamask here
2 | PRIVATE_KEY=
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | # Only exists if Bazel was run
8 | /bazel-out
9 |
10 | # dependencies
11 | /node_modules
12 |
13 | # profiling files
14 | chrome-profiler-events*.json
15 |
16 | # IDEs and editors
17 | /.idea
18 | .project
19 | .classpath
20 | .c9/
21 | *.launch
22 | .settings/
23 | *.sublime-workspace
24 |
25 | # IDE - VSCode
26 | .vscode/*
27 | !.vscode/settings.json
28 | !.vscode/tasks.json
29 | !.vscode/launch.json
30 | !.vscode/extensions.json
31 | .history/*
32 |
33 | # misc
34 | /.angular/cache
35 | /.sass-cache
36 | /connect.lock
37 | /coverage
38 | /libpeerconnection.log
39 | npm-debug.log
40 | yarn-error.log
41 | testem.log
42 | /typings
43 |
44 | # System Files
45 | .DS_Store
46 | Thumbs.db
47 |
48 | node_modules
49 | .env
50 | coverage
51 | coverage.json
52 | typechain
53 |
54 | #Hardhat files
55 | cache
56 | artifacts
57 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Patric Gutersohn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular Starter DApp Template
2 |
3 | This template uses following versions
4 |
5 | * Angular 13
6 | * Hardhat 2.8
7 | * Ethers 5.5
8 | * Solidity 0.8.4
9 |
10 | ## Getting started
11 |
12 | 1. Clone this repository
13 |
14 | ```
15 | git clone git@github.com:pguso/angular-hardhat-starter-dapp.git
16 | ```
17 |
18 | 2. Install dependencies
19 |
20 | ```
21 | npm install
22 | ```
23 |
24 | 3. Install Metamask (if you not already did) https://metamask.io/download/
25 | 4. Configure Polygon in Metamask https://docs.matic.today/docs/develop/metamask/config-polygon-on-metamask/ (switch to the Mumbai-Testnet Tab)
26 | 5. Rename .env.dist to .env and paste your Metamask private key into PRIVATE_KEY
27 | 6. Upload the contract to the Polygon Mumbai Testnet, make sure you have enough MATIC in Metamask (you can get test MATIC from here https://faucet.polygon.technology/)
28 |
29 | ```
30 | npm run deploy:testnet
31 | ```
32 |
33 | 7. Paste the contract address, you get on the command line, into src/environments/environment.ts
34 |
35 | ```typescript
36 | export const environment = {
37 | // ...
38 | contractAddress: '0x04215C89a6af0f7ed9103c48BaF6A8e19f119470',
39 | // ...
40 | };
41 | ```
42 |
43 | 8. Run angular application and test the app under http://localhost:4200
44 |
45 | ```
46 | ng serve
47 | ```
48 |
49 | ## This template has three pages
50 |
51 | ### The Homepage
52 |
53 | A list of all uploaded images.
54 |
55 | ### The authors page
56 |
57 | Only images are shown on this page that are uploaded by the selected account in Metamask.
58 |
59 | ### The upload form
60 |
61 | Here anybody who is connected via Metamask can upload an image with title and description. The image and the image metadata get uploaded to IPFS.
62 |
63 | ## Hardhat config
64 |
65 | The default config hardhat.config.js is configured for the Polygon Network. Here you find examples for other Blockchains.
66 |
67 | ### Binance Smart Chain
68 |
69 | ```javascript
70 | require('@nomiclabs/hardhat-waffle')
71 | require('dotenv').config()
72 |
73 | const privateKey = process.env.PRIVATE_KEY
74 |
75 | module.exports = {
76 | networks: {
77 | hardhat: {
78 | chainId: 1337,
79 | },
80 | testnet: {
81 | url: 'https://data-seed-prebsc-1-s1.binance.org:8545',
82 | chainId: 97,
83 | gasPrice: 20000000000,
84 | accounts: [privateKey],
85 | },
86 | mainnet: {
87 | url: 'https://bsc-dataseed.binance.org/',
88 | chainId: 56,
89 | gasPrice: 20000000000,
90 | accounts: [privateKey],
91 | },
92 | },
93 | solidity: '0.8.4',
94 | }
95 | ```
96 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "angular-dapp": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss"
11 | },
12 | "@schematics/angular:application": {
13 | "strict": true
14 | }
15 | },
16 | "root": "",
17 | "sourceRoot": "src",
18 | "prefix": "app",
19 | "architect": {
20 | "build": {
21 | "builder": "@angular-devkit/build-angular:browser",
22 | "options": {
23 | "outputPath": "dist/angular-dapp",
24 | "index": "src/index.html",
25 | "main": "src/main.ts",
26 | "polyfills": "src/polyfills.ts",
27 | "tsConfig": "tsconfig.app.json",
28 | "inlineStyleLanguage": "scss",
29 | "assets": [
30 | "src/favicon.ico",
31 | "src/assets"
32 | ],
33 | "styles": [
34 | "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
35 | "src/styles.scss"
36 | ],
37 | "scripts": []
38 | },
39 | "configurations": {
40 | "production": {
41 | "budgets": [
42 | {
43 | "type": "initial",
44 | "maximumWarning": "500kb",
45 | "maximumError": "1mb"
46 | },
47 | {
48 | "type": "anyComponentStyle",
49 | "maximumWarning": "2kb",
50 | "maximumError": "4kb"
51 | }
52 | ],
53 | "fileReplacements": [
54 | {
55 | "replace": "src/environments/environment.ts",
56 | "with": "src/environments/environment.prod.ts"
57 | }
58 | ],
59 | "outputHashing": "all"
60 | },
61 | "development": {
62 | "buildOptimizer": false,
63 | "optimization": false,
64 | "vendorChunk": true,
65 | "extractLicenses": false,
66 | "sourceMap": true,
67 | "namedChunks": true
68 | }
69 | },
70 | "defaultConfiguration": "production"
71 | },
72 | "serve": {
73 | "builder": "@angular-devkit/build-angular:dev-server",
74 | "configurations": {
75 | "production": {
76 | "browserTarget": "angular-dapp:build:production"
77 | },
78 | "development": {
79 | "browserTarget": "angular-dapp:build:development"
80 | }
81 | },
82 | "defaultConfiguration": "development"
83 | },
84 | "extract-i18n": {
85 | "builder": "@angular-devkit/build-angular:extract-i18n",
86 | "options": {
87 | "browserTarget": "angular-dapp:build"
88 | }
89 | },
90 | "test": {
91 | "builder": "@angular-devkit/build-angular:karma",
92 | "options": {
93 | "main": "src/test.ts",
94 | "polyfills": "src/polyfills.ts",
95 | "tsConfig": "tsconfig.spec.json",
96 | "karmaConfig": "karma.conf.js",
97 | "inlineStyleLanguage": "scss",
98 | "assets": [
99 | "src/favicon.ico",
100 | "src/assets"
101 | ],
102 | "styles": [
103 | "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
104 | "src/styles.scss"
105 | ],
106 | "scripts": []
107 | }
108 | }
109 | }
110 | }
111 | },
112 | "defaultProject": "angular-dapp"
113 | }
114 |
--------------------------------------------------------------------------------
/contracts/Gallery.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.7.0 <0.9.0;
3 |
4 | contract Gallery {
5 | Image[] private images;
6 | mapping(address => Image[]) private authorToImages;
7 |
8 | struct Image {
9 | string title;
10 | string imageMetaDataUrl;
11 | }
12 |
13 | function store(string memory title, string memory imageMetaDataUrl) public {
14 | Image memory image = Image(title, imageMetaDataUrl);
15 |
16 | images.push(image);
17 | authorToImages[msg.sender].push(image);
18 | }
19 |
20 | function retrieveAllImages() public view returns (Image[] memory) {
21 | return images;
22 | }
23 |
24 | function retrieveImagesByAuthor() public view returns (Image[] memory) {
25 | return authorToImages[msg.sender];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/hardhat.config.js:
--------------------------------------------------------------------------------
1 | require('@nomiclabs/hardhat-waffle')
2 | require('dotenv').config()
3 |
4 | const privateKey = process.env.PRIVATE_KEY
5 |
6 | module.exports = {
7 | networks: {
8 | hardhat: {
9 | chainId: 1337,
10 | },
11 | testnet: {
12 | url: 'https://rpc-mumbai.maticvigil.com',
13 | accounts: [privateKey],
14 | },
15 | mainnet: {
16 | url: 'https://polygon-rpc.com',
17 | accounts: [privateKey],
18 | },
19 | },
20 | solidity: '0.8.4',
21 | }
22 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | jasmine: {
17 | // you can add configuration options for Jasmine here
18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
19 | // for example, you can disable the random execution with `random: false`
20 | // or set a specific seed with `seed: 4321`
21 | },
22 | clearContext: false // leave Jasmine Spec Runner output visible in browser
23 | },
24 | jasmineHtmlReporter: {
25 | suppressAll: true // removes the duplicated traces
26 | },
27 | coverageReporter: {
28 | dir: require('path').join(__dirname, './coverage/angular-dapp'),
29 | subdir: '.',
30 | reporters: [
31 | { type: 'html' },
32 | { type: 'text-summary' }
33 | ]
34 | },
35 | reporters: ['progress', 'kjhtml'],
36 | port: 9876,
37 | colors: true,
38 | logLevel: config.LOG_INFO,
39 | autoWatch: true,
40 | browsers: ['Chrome'],
41 | singleRun: false,
42 | restartOnFileChange: true
43 | });
44 | };
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-dapp",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "watch": "ng build --watch --configuration development",
9 | "test": "npx hardhat test",
10 | "deploy:testnet": "npx hardhat run --network testnet scripts/deploy.js",
11 | "deploy:mainnet": "npx hardhat run --network mainnet scripts/deploy.js"
12 | },
13 | "private": true,
14 | "dependencies": {
15 | "@angular/animations": "~13.0.0",
16 | "@angular/cdk": "^13.1.0",
17 | "@angular/common": "~13.0.0",
18 | "@angular/compiler": "~13.0.0",
19 | "@angular/core": "~13.0.0",
20 | "@angular/forms": "~13.0.0",
21 | "@angular/material": "^13.1.0",
22 | "@angular/platform-browser": "~13.0.0",
23 | "@angular/platform-browser-dynamic": "~13.0.0",
24 | "@angular/router": "~13.0.0",
25 | "@metamask/detect-provider": "^1.2.0",
26 | "dotenv": "^10.0.0",
27 | "rxjs": "~7.4.0",
28 | "tslib": "^2.3.0",
29 | "zone.js": "~0.11.4",
30 | "ipfs-http-client": "^54.0.2"
31 | },
32 | "devDependencies": {
33 | "@angular-devkit/build-angular": "~13.0.2",
34 | "@angular/cli": "~13.0.2",
35 | "@angular/compiler-cli": "~13.0.0",
36 | "@nomiclabs/hardhat-ethers": "^2.0.3",
37 | "@nomiclabs/hardhat-waffle": "^2.0.1",
38 | "@openzeppelin/contracts": "^4.4.0",
39 | "@types/jasmine": "~3.10.0",
40 | "@types/node": "^12.11.1",
41 | "chai": "^4.3.4",
42 | "ethereum-waffle": "^3.4.0",
43 | "ethers": "^5.5.2",
44 | "hardhat": "^2.7.0",
45 | "jasmine-core": "~3.10.0",
46 | "karma": "~6.3.0",
47 | "karma-chrome-launcher": "~3.1.0",
48 | "karma-coverage": "~2.0.3",
49 | "karma-jasmine": "~4.0.0",
50 | "karma-jasmine-html-reporter": "~1.7.0",
51 | "typescript": "~4.4.3"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/scripts/deploy.js:
--------------------------------------------------------------------------------
1 | const hre = require('hardhat')
2 |
3 | async function main() {
4 | const Gallery = await hre.ethers.getContractFactory('Gallery')
5 | const gallery = await Gallery.deploy()
6 | await gallery.deployed()
7 |
8 | const txHash = gallery.deployTransaction.hash;
9 | const txReceipt = await hre.ethers.provider.waitForTransaction(txHash);
10 | console.log(`check your contract: https://mumbai.polygonscan.com/address/${txReceipt.contractAddress}`)
11 | console.log("contract address:", txReceipt.contractAddress);
12 | }
13 |
14 | main()
15 | .then(() => process.exit(0))
16 | .catch((error) => {
17 | console.error(error)
18 | process.exit(1)
19 | })
20 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 | import { HomeComponent } from "./components/home/home.component";
4 | import { ImagesByAuthorComponent } from "./components/images-by-author/images-by-author.component";
5 | import { UploadImageComponent } from "./components/upload-image/upload-image.component";
6 |
7 | const routes: Routes = [
8 | {'path': '', redirectTo: 'home', pathMatch: 'full'},
9 | {'path': 'home', component: HomeComponent},
10 | {'path': 'authors-images', component: ImagesByAuthorComponent},
11 | {'path': 'upload', component: UploadImageComponent},
12 | ];
13 |
14 | @NgModule({
15 | imports: [RouterModule.forRoot(routes)],
16 | exports: [RouterModule]
17 | })
18 | export class AppRoutingModule { }
19 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 | Image Gallery
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | .mat-toolbar {
2 | display: flex;
3 | justify-content: space-between;
4 | }
5 |
6 | main {
7 | margin: 0 auto;
8 | padding: 2rem;
9 | }
10 |
11 | .active {
12 | background: rgba(0,0,0,.2);
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 | import { AppComponent } from './app.component';
4 |
5 | describe('AppComponent', () => {
6 | beforeEach(async () => {
7 | await TestBed.configureTestingModule({
8 | imports: [
9 | RouterTestingModule
10 | ],
11 | declarations: [
12 | AppComponent
13 | ],
14 | }).compileComponents();
15 | });
16 |
17 | it('should create the app', () => {
18 | const fixture = TestBed.createComponent(AppComponent);
19 | const app = fixture.componentInstance;
20 | expect(app).toBeTruthy();
21 | });
22 |
23 | it(`should have as title 'angular-dapp'`, () => {
24 | const fixture = TestBed.createComponent(AppComponent);
25 | const app = fixture.componentInstance;
26 | expect(app.title).toEqual('angular-dapp');
27 | });
28 |
29 | it('should render title', () => {
30 | const fixture = TestBed.createComponent(AppComponent);
31 | fixture.detectChanges();
32 | const compiled = fixture.nativeElement as HTMLElement;
33 | expect(compiled.querySelector('.content span')?.textContent).toContain('angular-dapp app is running!');
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: './app.component.html',
6 | styleUrls: ['./app.component.scss']
7 | })
8 | export class AppComponent {
9 | title = 'angular-dapp';
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 |
4 | import { AppRoutingModule } from './app-routing.module';
5 | import { AppComponent } from './app.component';
6 | import { NoopAnimationsModule } from '@angular/platform-browser/animations';
7 | import { HomeComponent } from './components/home/home.component';
8 | import { ImagesByAuthorComponent } from './components/images-by-author/images-by-author.component';
9 | import { UploadImageComponent } from './components/upload-image/upload-image.component';
10 | import { MatToolbarModule } from "@angular/material/toolbar";
11 | import { HttpClientModule } from "@angular/common/http";
12 | import { ReactiveFormsModule } from "@angular/forms";
13 | import { MatFormFieldModule } from "@angular/material/form-field";
14 | import { MatIconModule } from "@angular/material/icon";
15 | import { MatInputModule } from "@angular/material/input";
16 | import { MatButtonModule } from "@angular/material/button";
17 | import { MatCardModule } from "@angular/material/card";
18 |
19 | @NgModule({
20 | declarations: [
21 | AppComponent,
22 | HomeComponent,
23 | ImagesByAuthorComponent,
24 | UploadImageComponent
25 | ],
26 | imports: [
27 | BrowserModule,
28 | AppRoutingModule,
29 | HttpClientModule,
30 | ReactiveFormsModule,
31 | NoopAnimationsModule,
32 | MatToolbarModule,
33 | MatFormFieldModule,
34 | MatIconModule,
35 | MatInputModule,
36 | MatButtonModule,
37 | MatCardModule
38 | ],
39 | providers: [],
40 | bootstrap: [ AppComponent ]
41 | })
42 | export class AppModule {
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/components/home/home.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{image.title}}
6 |
7 |
8 |
9 |
10 | {{image.description}}
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/app/components/home/home.component.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | &__images {
3 | display: grid;
4 | grid-template-columns: repeat(3, 1fr);
5 | gap: 1.5rem;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/components/home/home.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { HomeComponent } from './home.component';
4 |
5 | describe('HomeComponent', () => {
6 | let component: HomeComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ HomeComponent ]
12 | })
13 | .compileComponents();
14 | });
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(HomeComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/components/home/home.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { GalleryService } from "../../services/gallery.service";
3 | import { HttpClient } from "@angular/common/http";
4 |
5 | @Component({
6 | selector: 'app-home',
7 | templateUrl: './home.component.html',
8 | styleUrls: ['./home.component.scss']
9 | })
10 | export class HomeComponent implements OnInit {
11 | public images: any[] = []
12 |
13 | constructor(
14 | private gallery: GalleryService,
15 | private http: HttpClient
16 | ) { }
17 |
18 | public async ngOnInit(): Promise {
19 | const images = await this.gallery.getAllImages()
20 | this.images = await Promise.all(images.map(async (image) => {
21 | const metaData: any = await this.http.get(image.imageMetaDataUrl).toPromise()
22 | return {
23 | title: image.title,
24 | image: metaData.fileUrl,
25 | description: metaData.description
26 | }
27 | }))
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/components/images-by-author/images-by-author.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{image.title}}
6 |
7 |
8 |
9 |
10 | {{image.description}}
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/app/components/images-by-author/images-by-author.component.scss:
--------------------------------------------------------------------------------
1 | .author {
2 | &__images {
3 | display: grid;
4 | grid-template-columns: repeat(3, 1fr);
5 | gap: 1.5rem;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/components/images-by-author/images-by-author.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ImagesByAuthorComponent } from './images-by-author.component';
4 |
5 | describe('ImagesByAuthorComponent', () => {
6 | let component: ImagesByAuthorComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ ImagesByAuthorComponent ]
12 | })
13 | .compileComponents();
14 | });
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(ImagesByAuthorComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/components/images-by-author/images-by-author.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { GalleryService } from "../../services/gallery.service";
3 | import { HttpClient } from "@angular/common/http";
4 |
5 | @Component({
6 | selector: 'app-images-by-author',
7 | templateUrl: './images-by-author.component.html',
8 | styleUrls: ['./images-by-author.component.scss']
9 | })
10 | export class ImagesByAuthorComponent implements OnInit {
11 | public images: any[] = []
12 |
13 | constructor(
14 | private gallery: GalleryService,
15 | private http: HttpClient
16 | ) { }
17 |
18 | public async ngOnInit(): Promise {
19 | const images = await this.gallery.getImagesByAuthor()
20 | this.images = await Promise.all(images.map(async (image) => {
21 | const metaData: any = await this.http.get(image.imageMetaDataUrl).toPromise()
22 | return {
23 | title: image.title,
24 | image: metaData.fileUrl,
25 | description: metaData.description
26 | }
27 | }))
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/components/upload-image/upload-image.component.html:
--------------------------------------------------------------------------------
1 |
2 |
Upload new Image
3 |
{{formError}}
4 |
35 |
36 |
--------------------------------------------------------------------------------
/src/app/components/upload-image/upload-image.component.scss:
--------------------------------------------------------------------------------
1 | .upload {
2 | &__item {
3 | margin-bottom: 1rem;
4 | position: relative;
5 | }
6 |
7 | &__image {
8 | position: absolute;
9 | background-color: #23262f;
10 | height: 100%;
11 | }
12 |
13 | &__file {
14 | position: relative;
15 | display: flex;
16 | flex-direction: column;
17 | justify-content: center;
18 | align-items: center;
19 | height: 182px;
20 | margin-top: 16px;
21 | border-radius: 16px;
22 | overflow: hidden;
23 | background: #f4f5f6;
24 | }
25 |
26 | &__input {
27 | position: absolute;
28 | top: 0;
29 | left: 0;
30 | font-size: 400px;
31 | opacity: 0;
32 | }
33 | }
34 |
35 | .mat-form-field {
36 | width: 100%;
37 | }
38 |
39 | .error {
40 | color: #ff0000;
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/components/upload-image/upload-image.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { UploadImageComponent } from './upload-image.component';
4 |
5 | describe('UploadImageComponent', () => {
6 | let component: UploadImageComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ UploadImageComponent ]
12 | })
13 | .compileComponents();
14 | });
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(UploadImageComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/components/upload-image/upload-image.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core'
2 | import { IpfsService } from "../../services/ipfs.service"
3 | import { FormBuilder, Validators } from "@angular/forms"
4 | import { Router } from "@angular/router"
5 | import { GalleryService } from "../../services/gallery.service";
6 |
7 | @Component({
8 | selector: 'app-upload-image',
9 | templateUrl: './upload-image.component.html',
10 | styleUrls: ['./upload-image.component.scss']
11 | })
12 | export class UploadImageComponent {
13 | public uploadForm = this.fb.group({
14 | title: this.fb.control('', Validators.required),
15 | fileUrl: this.fb.control('', Validators.required),
16 | description: this.fb.control('', Validators.required),
17 | })
18 | public uploadedImage = ''
19 | public formError = ''
20 | public isLoading = false
21 |
22 | constructor(
23 | private ipfs: IpfsService,
24 | private fb: FormBuilder,
25 | private router: Router,
26 | private gallery: GalleryService
27 | ) { }
28 |
29 | public async uploadImage(eventTarget: any) {
30 | const fileUrl = await this.ipfs.uploadFile(eventTarget.files[0])
31 | this.uploadedImage = fileUrl
32 | this.uploadForm.get('fileUrl')?.setValue(fileUrl)
33 | }
34 |
35 | public async onSubmit() {
36 | if (this.uploadForm.valid) {
37 | this.isLoading = true
38 | const title = this.uploadForm.get('title')?.value
39 | const fileUrl = this.uploadForm.get('fileUrl')?.value
40 | const description = this.uploadForm.get('description')?.value
41 | const metaDataUrl = await this.ipfs.uploadFile(JSON.stringify({
42 | fileUrl,
43 | description
44 | }))
45 |
46 | const isItemCreated = await this.gallery.addImage(title, metaDataUrl)
47 |
48 | this.isLoading = false
49 | if (isItemCreated) {
50 | await this.router.navigate([ '/authors-images' ]);
51 | }
52 | } else {
53 | console.error('form is not valid')
54 | this.formError = 'Form is not valid'
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/app/services/gallery.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { GalleryService } from './gallery.service';
4 |
5 | describe('GalleryService', () => {
6 | let service: GalleryService;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(GalleryService);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/services/gallery.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { ethers } from "ethers";
3 | import { environment } from "../../environments/environment";
4 | import Gallery from '../../../artifacts/contracts/Gallery.sol/Gallery.json'
5 | import detectEthereumProvider from "@metamask/detect-provider";
6 |
7 | @Injectable({
8 | providedIn: 'root'
9 | })
10 | export class GalleryService {
11 | public async getAllImages(): Promise {
12 | const contract = await GalleryService.getContract()
13 |
14 | return await contract['retrieveAllImages']()
15 | }
16 |
17 | public async getImagesByAuthor(): Promise {
18 | const contract = await GalleryService.getContract(true)
19 |
20 | return await contract['retrieveImagesByAuthor']()
21 | }
22 |
23 | public async addImage(title: string, fileUrl: string): Promise {
24 | const contract = await GalleryService.getContract(true)
25 | const transaction = await contract['store'](
26 | title,
27 | fileUrl
28 | )
29 | const tx = await transaction.wait()
30 |
31 | return tx.status === 1
32 | }
33 |
34 | private static async getContract(bySigner=false) {
35 | const provider = await GalleryService.getWebProvider()
36 | const signer = provider.getSigner()
37 |
38 | return new ethers.Contract(
39 | environment.contractAddress,
40 | Gallery.abi,
41 | bySigner ? signer : provider,
42 | )
43 | }
44 |
45 | private static async getWebProvider(requestAccounts = true) {
46 | const provider: any = await detectEthereumProvider()
47 |
48 | if (requestAccounts) {
49 | await provider.request({ method: 'eth_requestAccounts' })
50 | }
51 |
52 | return new ethers.providers.Web3Provider(provider)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/app/services/ipfs.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { IpfsService } from './ipfs.service';
4 |
5 | describe('IpfsService', () => {
6 | let service: IpfsService;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(IpfsService);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/services/ipfs.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@angular/core"
2 | import { create } from "ipfs-http-client"
3 | import { environment } from "../../environments/environment"
4 |
5 | @Injectable({
6 | providedIn: 'root'
7 | })
8 | export class IpfsService {
9 | public async uploadFile(data: any): Promise {
10 | let url = ''
11 | const client = IpfsService.getClient()
12 |
13 | try {
14 | const added = await client.add(data)
15 | url = `${environment.ipfs}/ipfs/${added.path}`
16 | } catch (error) {
17 | console.log(error)
18 | }
19 |
20 | return url
21 | }
22 |
23 | private static getClient(): any {
24 | // @ts-ignore
25 | return create(`${environment.ipfs}:5001/api/v0`)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pguso/angular-hardhat-starter-dapp/f0aad8adf9009697f6fe1a63b9d21d885fb2fd53/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | contractAddress: '',
4 | ipfs: 'https://ipfs.infura.io'
5 | };
6 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false,
7 | contractAddress: '0x04215C89a6af0f7ed9103c48BaF6A8e19f119470',
8 | ipfs: 'https://ipfs.infura.io'
9 | };
10 |
11 | /*
12 | * For easier debugging in development mode, you can import the following file
13 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
14 | *
15 | * This import should be commented out in production mode because it will have a negative impact
16 | * on performance if an error is thrown.
17 | */
18 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
19 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pguso/angular-hardhat-starter-dapp/f0aad8adf9009697f6fe1a63b9d21d885fb2fd53/src/favicon.ico
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AngularDapp
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.error(err));
13 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including
12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /**
22 | * By default, zone.js will patch all possible macroTask and DomEvents
23 | * user can disable parts of macroTask/DomEvents patch by setting following flags
24 | * because those flags need to be set before `zone.js` being loaded, and webpack
25 | * will put import in the top of bundle, so user need to create a separate file
26 | * in this directory (for example: zone-flags.ts), and put the following flags
27 | * into that file, and then add the following code before importing zone.js.
28 | * import './zone-flags';
29 | *
30 | * The flags allowed in zone-flags.ts are listed here.
31 | *
32 | * The following flags will work for all browsers.
33 | *
34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
37 | *
38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
40 | *
41 | * (window as any).__Zone_enable_cross_context_check = true;
42 | *
43 | */
44 |
45 | /***************************************************************************************************
46 | * Zone JS is required by default for Angular itself.
47 | */
48 | import 'zone.js'; // Included with Angular CLI.
49 |
50 |
51 | /***************************************************************************************************
52 | * APPLICATION IMPORTS
53 | */
54 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
3 | html, body { height: 100%; }
4 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
5 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: {
11 | context(path: string, deep?: boolean, filter?: RegExp): {
12 | keys(): string[];
13 | (id: string): T;
14 | };
15 | };
16 |
17 | // First, initialize the Angular testing environment.
18 | getTestBed().initTestEnvironment(
19 | BrowserDynamicTestingModule,
20 | platformBrowserDynamicTesting(),
21 | );
22 |
23 | // Then we find all the tests.
24 | const context = require.context('./', true, /\.spec\.ts$/);
25 | // And load the modules.
26 | context.keys().map(context);
27 |
--------------------------------------------------------------------------------
/test/sample-test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require("chai");
2 | const { ethers } = require("hardhat");
3 |
4 | describe("Greeter", function () {
5 | it("Should return the new greeting once it's changed", async function () {
6 | const Greeter = await ethers.getContractFactory("Greeter");
7 | const greeter = await Greeter.deploy("Hello, world!");
8 | await greeter.deployed();
9 |
10 | expect(await greeter.greet()).to.equal("Hello, world!");
11 |
12 | const setGreetingTx = await greeter.setGreeting("Hola, mundo!");
13 |
14 | // wait until the transaction is mined
15 | await setGreetingTx.wait();
16 |
17 | expect(await greeter.greet()).to.equal("Hola, mundo!");
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/app",
6 | "types": []
7 | },
8 | "files": [
9 | "src/main.ts",
10 | "src/polyfills.ts"
11 | ],
12 | "include": [
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "outDir": "./dist/out-tsc",
7 | "forceConsistentCasingInFileNames": true,
8 | "strict": true,
9 | "noImplicitOverride": true,
10 | "noPropertyAccessFromIndexSignature": true,
11 | "noImplicitReturns": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "sourceMap": true,
14 | "declaration": false,
15 | "downlevelIteration": true,
16 | "experimentalDecorators": true,
17 | "moduleResolution": "node",
18 | "importHelpers": true,
19 | "target": "es2017",
20 | "module": "es2020",
21 | "lib": [
22 | "es2020",
23 | "dom"
24 | ],
25 | "allowSyntheticDefaultImports": true,
26 | "resolveJsonModule": true
27 | },
28 | "angularCompilerOptions": {
29 | "enableI18nLegacyMessageIdFormat": false,
30 | "strictInjectionParameters": true,
31 | "strictInputAccessModifiers": true,
32 | "strictTemplates": true
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/spec",
6 | "types": [
7 | "jasmine"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts",
12 | "src/polyfills.ts"
13 | ],
14 | "include": [
15 | "src/**/*.spec.ts",
16 | "src/**/*.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------